Files
orchestra/internal/operations/submission_test.go
T
kami 7f12c7fc37 v3 workflow: intent, phases, review, submission, enforcement, burn-in
The v3 stack, previously an uncommitted working tree, plus this session's two
units and the burn-in instrument. This commit is the burn-in build identity:
coordinator and worker must both report this revision before a task is created.

Workflow (earlier sessions, uncommitted until now): human decision events and
reduction, source cursors and reconcile-before-launch, turn-boundary
reconciliation, internal/agentctx as the single renderer, ace-fca phases with
sealed artifacts, the trajectory gate, bounded grilling, independent review,
task pr enforcement, and human review reflection.

Capability restrictions at the agent boundary: an authz.Agent surface at
GatedWrite may ask and may not act. It also fixes two bugs the unit exposed --
gated surfaces could not reach the two endpoints written for them, and
RequestHumanDecision would block an unowned task while rejecting a question
from the session that did own it.

Turn-boundary reconcile-failure escalation: a streak of consecutive failures
asks the session to hand off, fenced on the lease epoch, with reconcile_failure
as a real handoff reason. The worker was dropping the coordinator's verdict on
the floor; it now acts on it.

Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to
<worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md
is the runbook. deploy/build.sh stamps both binaries from one commit.

go build, go vet and go test ./... pass, 20 packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 18:31:20 +04:00

330 lines
10 KiB
Go

package operations
import (
"context"
"errors"
"strings"
"testing"
"orchestra/internal/domain"
"orchestra/internal/registry"
"orchestra/internal/review"
"orchestra/internal/store"
)
type fakePublisher struct {
pushes int
prCalls int
remoteSHA string
pushErr error
prErr error
pr domain.ExternalRef
lastBody string
}
func (f *fakePublisher) Push(_ context.Context, _, _, sha string) (string, error) {
f.pushes++
if f.pushErr != nil {
return "", f.pushErr
}
if f.remoteSHA != "" {
return f.remoteSHA, nil
}
return sha, nil
}
func (f *fakePublisher) EnsurePR(_ context.Context, plan SubmissionPlan) (domain.ExternalRef, error) {
f.prCalls++
f.lastBody = plan.PRBody
if f.prErr != nil {
return domain.ExternalRef{}, f.prErr
}
if f.pr.ID == "" {
f.pr = domain.ExternalRef{Provider: "gitea:p", ID: "142", URL: "https://git/pulls/142"}
}
return f.pr, nil
}
func gate(sha string) domain.GateResult {
return domain.GateResult{Command: "go test ./...", ExitCode: 0, SHA: sha, Output: "ok"}
}
// reviewed walks a task to a reviewed state at one commit.
func reviewed(t *testing.T, findings ...review.Finding) (*store.Store, string, registry.Project) {
t.Helper()
project := registry.Project{ID: "p", QualityGate: "go test ./..."}
s, id := atImplement(t, project)
if _, err := EnterReview(s, project, id, evidence(shaA)); err != nil {
t.Fatal(err)
}
if _, err := RecordReview(s, project, id, review.Result{ResultSHA: shaA, Findings: findings}); err != nil {
t.Fatal(err)
}
return s, id, project
}
func head(sha string) HeadResolver {
return func(context.Context) (string, error) { return sha, nil }
}
func TestSubmissionEligibility(t *testing.T) {
// Reviewed A, head A, gate A: allowed.
s, id, project := reviewed(t)
task, _ := s.Task(id)
if check := domain.CheckSubmission(task, shaA, gate(shaA)); !check.Eligible {
t.Fatalf("want eligible, got %v", check.Reasons)
}
// The three shas must agree.
cases := map[string]struct {
head string
g domain.GateResult
}{
"head moved past the review": {shaB, gate(shaB)},
"gate ran on another commit": {shaB, gate(shaA)},
"failing gate": {shaA, domain.GateResult{Command: "go test ./...", ExitCode: 1, SHA: shaA}},
"unanchored head": {"short", gate("short")},
}
for name, c := range cases {
if check := domain.CheckSubmission(task, c.head, c.g); check.Eligible {
t.Fatalf("%s: want refusal", name)
}
}
if _, err := PrepareSubmission(s, project, id, shaB, gate(shaB), Notes{}); !errors.Is(err, ErrNotSubmittable) {
t.Fatalf("want ErrNotSubmittable, got %v", err)
}
// Minor findings do not block. Important findings do.
s, id, project = reviewed(t, finding("f1", review.Minor))
task, _ = s.Task(id)
if check := domain.CheckSubmission(task, shaA, gate(shaA)); !check.Eligible {
t.Fatalf("minor-only: want eligible, got %v", check.Reasons)
}
s, id, project = reviewed(t, finding("f1", review.Important))
task, _ = s.Task(id)
check := domain.CheckSubmission(task, shaA, gate(shaA))
if check.Eligible {
t.Fatal("an important finding must refuse submission")
}
if !strings.Contains(strings.Join(check.Reasons, " "), "unresolved blocker or important") {
t.Fatalf("reasons = %v", check.Reasons)
}
// A pending human decision refuses.
s, id, project = reviewed(t)
lease(t, s, id)
if _, err := RequestHumanDecision(s, project, id, request("which behaviour?")); err != nil {
t.Fatal(err)
}
task, _ = s.Task(id)
if domain.CheckSubmission(task, shaA, gate(shaA)).Eligible {
t.Fatal("an outstanding question must refuse submission")
}
// A project whose path includes plan but sealed none refuses.
bare, bareID := phaseStore(t)
if got, _ := bare.Task(bareID); len(got.RequirePhaseArtifacts(project.Phases())) != 2 {
t.Fatal("missing research and plan should both be reported")
}
}
func TestSubmissionRecordsExactIdentityAndIsIdempotent(t *testing.T) {
s, id, project := reviewed(t, finding("f1", review.Minor))
pub := &fakePublisher{}
plan, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{
BehaviouralChanges: []string{"lookups now use the index"},
Risks: []string{"index rebuild on first boot"},
Hotspots: []string{"internal/attr/attr.go:81-140 concurrency semantics changed"},
})
if err != nil {
t.Fatal(err)
}
e, err := ExecuteSubmission(context.Background(), s, plan, pub, head(shaA))
if err != nil {
t.Fatal(err)
}
if e.Type != domain.EventTaskSubmitted {
t.Fatalf("event = %+v", e)
}
got, _ := s.Task(id)
if got.State != domain.StateInReview {
t.Fatalf("state = %s, want in_review", got.State)
}
if got.Submission == nil {
t.Fatal("no submission recorded")
}
if got.Submission.ResultSHA != shaA || got.Submission.PR.ID != "142" || got.Submission.ReviewRef == "" || got.Submission.GateRef == "" {
t.Fatalf("submission = %+v", got.Submission)
}
if got.Submission.RemoteRef != "origin/orchestra/"+id {
t.Fatalf("remote ref = %q", got.Submission.RemoteRef)
}
// Submission is not completion.
if got.State == domain.StateCompleted {
t.Fatal("submission must not complete the task")
}
// The packet is derived, and labels the agent's account as unverified.
packet, err := s.Artifact(got.Submission.PacketRef)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"## Goal", "## Acceptance", "## Verification", "`go test ./...` exited 0",
"independent review of " + shaA, "minor findings, not fixed: 1",
"lookups now use the index", "index rebuild on first boot",
"internal/attr/attr.go:81-140", "are not verified",
} {
if !strings.Contains(string(packet), want) {
t.Fatalf("packet missing %q:\n%s", want, packet)
}
}
// Running it again with unchanged state is the same submission.
plan2, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{})
if err != nil {
t.Fatalf("a repeated submission must not be refused: %v", err)
}
if plan2.Existing == nil || plan2.Existing.PR.ID != "142" {
t.Fatalf("the existing submission was not carried into the plan: %+v", plan2.Existing)
}
e2, err := ExecuteSubmission(context.Background(), s, plan2, pub, head(shaA))
if err != nil {
t.Fatal(err)
}
if e2.ID != "" {
t.Fatal("a second TaskSubmitted was appended for the same commit")
}
if pub.prCalls != 2 || pub.pr.ID != "142" {
t.Fatalf("pr calls=%d id=%s: a retry must refresh one pull request", pub.prCalls, pub.pr.ID)
}
}
// A forge failure after a successful push must stay retryable.
func TestPRFailureLeavesTaskRetryable(t *testing.T) {
s, id, project := reviewed(t)
pub := &fakePublisher{prErr: errors.New("gitea 502")}
plan, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{})
if err != nil {
t.Fatal(err)
}
if _, err := ExecuteSubmission(context.Background(), s, plan, pub, head(shaA)); err == nil {
t.Fatal("expected the forge failure to surface")
}
got, _ := s.Task(id)
if got.State == domain.StateFailed || got.State == domain.StateInReview {
t.Fatalf("a transport failure changed the lifecycle: %s", got.State)
}
if got.Submission != nil {
t.Fatal("a failed submission was recorded")
}
// The retry re-pushes, verifies, and succeeds.
pub.prErr = nil
plan, err = PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{})
if err != nil {
t.Fatal(err)
}
if _, err := ExecuteSubmission(context.Background(), s, plan, pub, head(shaA)); err != nil {
t.Fatal(err)
}
if got, _ := s.Task(id); got.Submission == nil || got.Submission.ResultSHA != shaA {
t.Fatalf("retry did not record the submission")
}
if pub.pushes != 2 {
t.Fatalf("pushes = %d, the retry must re-verify the remote", pub.pushes)
}
}
// The remote holding a different commit is a hard refusal.
func TestRemoteMismatchRecordsNothing(t *testing.T) {
s, id, project := reviewed(t)
pub := &fakePublisher{remoteSHA: shaB}
plan, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{})
if err != nil {
t.Fatal(err)
}
if _, err := ExecuteSubmission(context.Background(), s, plan, pub, head(shaA)); !errors.Is(err, ErrRemoteMismatch) {
t.Fatalf("want ErrRemoteMismatch, got %v", err)
}
if pub.prCalls != 0 {
t.Fatal("a pull request was opened for an unverified push")
}
if got, _ := s.Task(id); got.Submission != nil {
t.Fatal("a mismatched submission was recorded")
}
}
// The commit is re-read immediately before the push and again before the
// event, so a tree that moves mid-submission cannot be submitted.
func TestHeadMovingDuringSubmissionRefuses(t *testing.T) {
s, id, project := reviewed(t)
plan, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{})
if err != nil {
t.Fatal(err)
}
pub := &fakePublisher{}
if _, err := ExecuteSubmission(context.Background(), s, plan, pub, head(shaB)); !errors.Is(err, ErrNotSubmittable) {
t.Fatalf("want refusal before push, got %v", err)
}
if pub.pushes != 0 {
t.Fatal("pushed a commit that was no longer head")
}
// Moves after the push, before the record.
calls := 0
moving := func(context.Context) (string, error) {
calls++
if calls == 1 {
return shaA, nil
}
return shaB, nil
}
if _, err := ExecuteSubmission(context.Background(), s, plan, pub, moving); !errors.Is(err, ErrNotSubmittable) {
t.Fatalf("want refusal before recording, got %v", err)
}
if got, _ := s.Task(id); got.Submission != nil {
t.Fatal("recorded a submission for a stale commit")
}
}
// A change after submission means the recorded submission no longer represents
// the current head.
func TestLaterChangeInvalidatesTheSubmission(t *testing.T) {
s, id, project := reviewed(t)
plan, err := PrepareSubmission(s, project, id, shaA, gate(shaA), Notes{})
if err != nil {
t.Fatal(err)
}
if _, err := ExecuteSubmission(context.Background(), s, plan, &fakePublisher{}, head(shaA)); err != nil {
t.Fatal(err)
}
got, _ := s.Task(id)
if !got.Submitted(shaA) {
t.Fatal("submission missing for its own commit")
}
if got.Submitted(shaB) {
t.Fatal("a submission of one commit must not cover another")
}
if domain.CheckSubmission(got, shaB, gate(shaB)).Eligible {
t.Fatal("a new commit must not inherit the old review and submission")
}
}
func TestNotesAreBounded(t *testing.T) {
s, id, project := reviewed(t)
long := strings.Repeat("x", 501)
bad := []Notes{
{Risks: []string{""}},
{Risks: []string{long}},
{Risks: []string{"one\ntwo"}},
{Hotspots: make([]string, 13)},
}
for i, n := range bad {
if _, err := PrepareSubmission(s, project, id, shaA, gate(shaA), n); !errors.Is(err, domain.ErrInvalid) {
t.Fatalf("notes %d: want ErrInvalid, got %v", i, err)
}
}
}