package orchestrator_test import ( "context" "encoding/json" "errors" "orchestra/internal/authz" "orchestra/internal/continuity" "orchestra/internal/domain" "orchestra/internal/herdr" "orchestra/internal/orchestrator" "orchestra/internal/store" "os" "os/exec" "testing" "time" ) type fakeAdapter struct { occupancy float64 boundary bool ref string releases int } func (a *fakeAdapter) Lease(_ context.Context, _ string, worktree string) (herdr.Session, error) { return herdr.Session{Harness: "h1", PaneID: "pane-1", Worktree: worktree}, nil } func (a *fakeAdapter) Bootstrap(context.Context, herdr.Session, string) error { return nil } func (a *fakeAdapter) Release(context.Context, herdr.Session) (string, error) { a.releases++ return a.ref, nil } func (a *fakeAdapter) Kill(context.Context, herdr.Session) error { return nil } func (a *fakeAdapter) Occupancy(herdr.Session) (float64, error) { return a.occupancy, nil } func (a *fakeAdapter) AtTurnBoundary(context.Context, herdr.Session) (bool, error) { return a.boundary, nil } type worktrees struct{ path string } func (w worktrees) Create(context.Context, domain.Task) (string, error) { return w.path, nil } type adapters struct{ a herdr.Adapter } func (a adapters) Adapter(string) (herdr.Adapter, error) { return a.a, nil } func run(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %v: %v: %s", args, err, out) } } // TestRotationEmitsValidReleaseWithAnchorSHA guards the highest-priority spec // defect noted in progress.md: automated rotation must emit a TaskReleased // event that satisfies domain.ValidatePayload (handoff_ref + anchor_sha), not // a payload missing anchor_sha that silently fails to append. func TestRotationEmitsValidReleaseWithAnchorSHA(t *testing.T) { repo := t.TempDir() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") head, err := herdr.HeadSHA(repo) if err != nil { t.Fatal(err) } s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] ref, err := s.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } a := &fakeAdapter{occupancy: .95, boundary: true, ref: ref} c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { if got, ok := s.Task(task.ID); ok && got.State == domain.StateQueued { break } time.Sleep(time.Millisecond) } got, ok := s.Task(task.ID) if !ok || got.State != domain.StateQueued { t.Fatalf("rotation did not complete: state=%v ok=%v", got.State, ok) } if a.releases == 0 { t.Fatalf("adapter Release was never invoked") } // Walk raw events to confirm the coordinator itself wrote a valid // TaskReleased payload with anchor_sha == the worktree's real HEAD. found := false for _, e := range s.Events(0) { if e.TaskID != task.ID || e.Type != "TaskReleased" { continue } var p map[string]any if err := json.Unmarshal(e.Payload, &p); err != nil { t.Fatal(err) } if err := domain.ValidatePayload("TaskReleased", p); err != nil { t.Fatalf("coordinator emitted invalid TaskReleased: %v (%v)", err, p) } if p["anchor_sha"] != head { t.Fatalf("anchor_sha=%v want=%s", p["anchor_sha"], head) } found = true } if !found { t.Fatal("coordinator never emitted a TaskReleased event") } } func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b } // TestTurnDecision guards AUDIT.md Phase 2 items 1-2: the synchronous, // per-turn counterpart to rotate() must return the same verdicts the // periodic ticker would compute, and rotate_now must actually perform the // release (not just report what rotate() would eventually do). func TestTurnDecision(t *testing.T) { repo := t.TempDir() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") newCoordinator := func(a *fakeAdapter) (*orchestrator.Coordinator, *store.Store, domain.Task) { s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json", Hard: .8} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } return c, s, task } t.Run("continue below threshold", func(t *testing.T) { a := &fakeAdapter{occupancy: .5} c, _, task := newCoordinator(a) decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnContinue { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnContinue) } }) t.Run("refuse when not at turn boundary", func(t *testing.T) { a := &fakeAdapter{occupancy: .95, boundary: false} c, _, task := newCoordinator(a) decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnRefuse { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnRefuse) } }) t.Run("rotate_now releases and emits a valid TaskReleased", func(t *testing.T) { a := &fakeAdapter{occupancy: .95, boundary: true} c, st, task := newCoordinator(a) artifactRef, err := st.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } a.ref = artifactRef decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnRotateNow { t.Fatalf("decision=%q want %q releases=%d", decision, orchestrator.TurnRotateNow, a.releases) } if a.releases == 0 { t.Fatal("adapter Release was never invoked") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateQueued { t.Fatalf("task state=%v ok=%v, want queued", got.State, ok) } }) t.Run("prepare_handoff at soft threshold, below hard, without a turn boundary", func(t *testing.T) { a := &handoffRequestingAdapter{fakeAdapter: fakeAdapter{occupancy: .6, boundary: false}} c, st, task := newCoordinator(&a.fakeAdapter) c.Adapters = adapters{a} decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnPrepareHandoff { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnPrepareHandoff) } if a.requests == 0 { t.Fatal("RequestHandoff was never invoked at the soft threshold") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateLeased { t.Fatalf("task state=%v ok=%v, want still leased (soft threshold is advisory)", got.State, ok) } }) t.Run("prepare_handoff requests handoff without releasing", func(t *testing.T) { a := &handoffRequestingAdapter{fakeAdapter: fakeAdapter{occupancy: .95, boundary: true}} c, st, task := newCoordinator(&a.fakeAdapter) c.Adapters = adapters{a} decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnPrepareHandoff { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnPrepareHandoff) } if a.releases != 0 { t.Fatal("adapter Release was invoked, expected only a handoff request") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateLeased { t.Fatalf("task state=%v ok=%v, want leased", got.State, ok) } }) // Agent-initiated ROTATE (spec §5.3): a handoff the agent wrote with // reason=manual is itself the boundary signal ("a coherent unit // finished and the next is independent") — it must release immediately, // bypassing occupancy and the turn-boundary probe entirely, not wait for // either to agree. t.Run("manual reason bypasses occupancy and turn boundary", func(t *testing.T) { a := &fakeAdapter{occupancy: 0, boundary: false} c, st, task := newCoordinator(a) artifactRef, err := st.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } a.ref = artifactRef head, err := herdr.HeadSHA(repo) if err != nil { t.Fatal(err) } handoff := map[string]any{ "meta": map[string]any{"id": "h2", "reason": "manual", "rotation_index": 0}, "anchor": map[string]any{"git_sha": head, "branch": "orchestra/t1"}, "goal": "g", "done_when": []string{"x"}, "action": "prompt", "command": "go test ./...", } b, err := json.Marshal(handoff) if err != nil { t.Fatal(err) } handoffPath := repo + "/" + herdr.HandoffFile if err := os.WriteFile(handoffPath, b, 0o644); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Remove(handoffPath) }) decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnRotateNow { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnRotateNow) } if a.releases == 0 { t.Fatal("Release was never invoked for an agent-initiated manual rotate") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateQueued { t.Fatalf("task state=%v ok=%v, want queued", got.State, ok) } }) } // TestStartBlocksOnInvalidPickup guards AUDIT.md's B6/Phase 4 item 4: // Coordinator.Start must run §6.2 pickup validation against the real // worktree before bootstrapping a successor onto a handoff_ref, and refuse // (TaskBlocked) rather than bootstrap on a mismatched anchor. func TestStartBlocksOnInvalidPickup(t *testing.T) { repo := t.TempDir() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] // A handoff whose anchor doesn't match anything in this fresh repo. badHandoff := map[string]any{ "meta": map[string]any{"id": "h1", "reason": "manual", "rotation_index": 0}, "anchor": map[string]any{"git_sha": strings0(40, 'a'), "branch": "orchestra/t1"}, "goal": "g", "done_when": []string{"x"}, "action": "prompt", "command": "go test ./...", } ref, err := s.PutArtifact(mustJSON(badHandoff)) if err != nil { t.Fatal(err) } a := &fakeAdapter{occupancy: 0} wt := orchestrator.GitWorktrees{Root: t.TempDir(), Repo: repo} c := &orchestrator.Coordinator{Store: s, Worktrees: wt, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } b, _ := json.Marshal(map[string]string{"harness_id": "h1", "handoff_ref": ref}) leaseEvt.Payload = b if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } got, ok := s.Task(task.ID) if !ok || got.State != domain.StateBlocked { t.Fatalf("expected TaskBlocked on invalid pickup, got state=%v ok=%v", got.State, ok) } } func strings0(n int, c byte) string { b := make([]byte, n) for i := range b { b[i] = c } return string(b) } // keyedHarnessAdapter reports Session.Harness as the harness kind ("claude"), // distinct from the herdr instance id ("homesrv-claude") under which it is // registered in AdapterFactory.Herdrs — reproducing production's real key // mismatch (adapters are keyed by herdr instance id; CLIAdapter.Lease sets // Session.Harness to the harness kind). type keyedHarnessAdapter struct{ fakeAdapter } func (a *keyedHarnessAdapter) Lease(_ context.Context, _ string, worktree string) (herdr.Session, error) { return herdr.Session{Harness: "claude", PaneID: "pane-1", Worktree: worktree}, nil } // TestAdapterResolvedByHerdrIDNotHarnessKind guards B2: AdapterFactory.Herdrs // is keyed by herdr instance id (e.g. "homesrv-claude"), never by the // harness kind Session.Harness holds (e.g. "claude"). Reconcile, expire, and // rotate must all resolve the adapter via Session.HerdrID (set at lease // time), not Session.Harness, or every one of them silently no-ops via a // bare Adapter-not-registered continue. func TestAdapterResolvedByHerdrIDNotHarnessKind(t *testing.T) { repo := t.TempDir() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] ref, err := s.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } a := &keyedHarnessAdapter{fakeAdapter{occupancy: .95, boundary: true, ref: ref}} factory := orchestrator.AdapterFactory{Herdrs: map[string]herdr.Adapter{"homesrv-claude": a}} c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: factory, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "homesrv-claude", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { if got, ok := s.Task(task.ID); ok && got.State == domain.StateQueued { break } time.Sleep(time.Millisecond) } got, ok := s.Task(task.ID) if !ok || got.State != domain.StateQueued { t.Fatalf("rotation did not complete via herdr-id-keyed adapter: state=%v ok=%v", got.State, ok) } if a.releases == 0 { t.Fatalf("adapter Release was never invoked — adapter lookup used Session.Harness instead of Session.HerdrID") } } // erroringBoundaryAdapter supports Face B but its probe always fails — this // must block release (never silently treat an unanswerable boundary check // as safe to interrupt), unlike an adapter that doesn't implement the // interface at all. type erroringBoundaryAdapter struct{ fakeAdapter } func (a *erroringBoundaryAdapter) AtTurnBoundary(context.Context, herdr.Session) (bool, error) { return false, errors.New("pane.status unsupported") } // noBoundaryAdapter never implements herdr.TurnBoundary at all, exercising // the genuine occupancy-only degraded fallback. type noBoundaryAdapter struct { occupancy float64 ref string releases int } func (a *noBoundaryAdapter) Lease(_ context.Context, _ string, worktree string) (herdr.Session, error) { return herdr.Session{Harness: "h1", PaneID: "pane-1", Worktree: worktree}, nil } func (a *noBoundaryAdapter) Bootstrap(context.Context, herdr.Session, string) error { return nil } func (a *noBoundaryAdapter) Release(context.Context, herdr.Session) (string, error) { a.releases++ return a.ref, nil } func (a *noBoundaryAdapter) Kill(context.Context, herdr.Session) error { return nil } func (a *noBoundaryAdapter) Occupancy(herdr.Session) (float64, error) { return a.occupancy, nil } func setupRotationTask(t *testing.T, repo string) (*store.Store, string, domain.Task, string) { t.Helper() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") head, err := herdr.HeadSHA(repo) if err != nil { t.Fatal(err) } s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] ref, err := s.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } return s, head, task, ref } // TestTurnBoundaryErrorBlocksRelease proves an adapter that implements Face B // but cannot currently answer it (a transient herdr error) never falls // through to an unconfirmed release — spec §5.2/§5.3 treats the boundary // check as required, not best-effort. func TestTurnBoundaryErrorBlocksRelease(t *testing.T) { repo := t.TempDir() s, _, task, ref := setupRotationTask(t, repo) a := &erroringBoundaryAdapter{fakeAdapter{occupancy: .95, boundary: true, ref: ref}} c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) time.Sleep(50 * time.Millisecond) got, _ := s.Task(task.ID) if got.State != domain.StateLeased { t.Fatalf("release proceeded despite an unanswerable turn-boundary check: state=%s", got.State) } if a.releases != 0 { t.Fatalf("adapter.Release was called despite the boundary error, releases=%d", a.releases) } if c.MonitorHealth().TurnBoundaryDegraded == 0 { t.Fatal("turn-boundary degradation was not recorded") } } // TestNoTurnBoundarySupportDegradesVisibly proves an adapter that never // implements Face B still falls back to occupancy-only thresholding (so // existing deployments keep working) but the degradation is observable via // MonitorHealth, not silent. func TestNoTurnBoundarySupportDegradesVisibly(t *testing.T) { repo := t.TempDir() s, head, task, ref := setupRotationTask(t, repo) a := &noBoundaryAdapter{occupancy: .95, ref: ref} c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { if got, ok := s.Task(task.ID); ok && got.State == domain.StateQueued { break } time.Sleep(time.Millisecond) } got, ok := s.Task(task.ID) if !ok || got.State != domain.StateQueued { t.Fatalf("rotation did not complete without Face B support: state=%v ok=%v", got.State, ok) } _ = head if c.MonitorHealth().TurnBoundaryDegraded == 0 { t.Fatal("missing Face B support was not recorded as degraded") } } type handoffRequestingAdapter struct { fakeAdapter requests int } func (a *handoffRequestingAdapter) RequestHandoff(context.Context, herdr.Session) error { a.requests++ return nil } // activityAdapter is fakeAdapter plus S11's milestone/thrash pair: it reports // a fixed tool-call history and records reasoned handoff requests, so tests // can drive checkActivityTriggers without a real herdr transcript. type activityAdapter struct { fakeAdapter calls []herdr.ToolCall activityErr error reasonAsked []string deadEndsSeen []continuity.DeadEnd } func (a *activityAdapter) Activity(context.Context, herdr.Session) ([]herdr.ToolCall, error) { return a.calls, a.activityErr } func (a *activityAdapter) RequestHandoffReason(_ context.Context, _ herdr.Session, reason string, deadEnds []continuity.DeadEnd) error { a.reasonAsked = append(a.reasonAsked, reason) a.deadEndsSeen = deadEnds return nil } // TestActivityTriggersRequestReasonedHandoffWithoutReleasing guards S11's two // orchestrator-detected rotation triggers (milestone, thrash): both rotate() // and TurnDecision must ask for a reasoned handoff — never release — the // first time the trigger fires, entirely independent of occupancy (both // cases here use occupancy=0, far below even the soft threshold). func TestActivityTriggersRequestReasonedHandoffWithoutReleasing(t *testing.T) { repo := t.TempDir() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") newCoordinator := func(a *activityAdapter) (*orchestrator.Coordinator, *store.Store, domain.Task) { s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json", Hard: .8} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } return c, s, task } thrashCalls := []herdr.ToolCall{ {Name: "Bash", Kind: "command", Key: "go test ./...", Success: false, IsTest: true}, {Name: "Bash", Kind: "command", Key: "go test ./...", Success: false, IsTest: true}, {Name: "Bash", Kind: "command", Key: "go test ./...", Success: false, IsTest: true}, } milestoneCalls := []herdr.ToolCall{ {Name: "Bash", Kind: "command", Key: "git commit -m done", Success: true}, } t.Run("thrash requests a reasoned handoff and does not release, via TurnDecision", func(t *testing.T) { a := &activityAdapter{fakeAdapter: fakeAdapter{occupancy: 0, boundary: true}, calls: thrashCalls} c, st, task := newCoordinator(a) decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnPrepareHandoff { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnPrepareHandoff) } if len(a.reasonAsked) != 1 || a.reasonAsked[0] != "thrash" { t.Fatalf("reasonAsked=%v want [thrash]", a.reasonAsked) } if len(a.deadEndsSeen) == 0 { t.Fatal("want populated dead ends for the thrash trigger") } if a.releases != 0 { t.Fatal("Release must not be invoked on a bare thrash detection") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateLeased { t.Fatalf("task state=%v ok=%v, want still leased", got.State, ok) } }) t.Run("milestone requests a reasoned handoff and does not release, via TurnDecision", func(t *testing.T) { a := &activityAdapter{fakeAdapter: fakeAdapter{occupancy: 0, boundary: true}, calls: milestoneCalls} c, st, task := newCoordinator(a) decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnPrepareHandoff { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnPrepareHandoff) } if len(a.reasonAsked) != 1 || a.reasonAsked[0] != "milestone" { t.Fatalf("reasonAsked=%v want [milestone]", a.reasonAsked) } if a.releases != 0 { t.Fatal("Release must not be invoked on a bare milestone detection") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateLeased { t.Fatalf("task state=%v ok=%v, want still leased", got.State, ok) } }) // Once the agent has actually written a thrash/milestone-reasoned // handoff, that reason is itself the boundary signal (same as manual) — // TurnDecision must release immediately, bypassing occupancy/boundary. t.Run("a written thrash handoff bypasses occupancy and releases", func(t *testing.T) { a := &activityAdapter{fakeAdapter: fakeAdapter{occupancy: 0, boundary: false}, calls: thrashCalls} c, st, task := newCoordinator(a) artifactRef, err := st.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } a.ref = artifactRef head, err := herdr.HeadSHA(repo) if err != nil { t.Fatal(err) } handoff := map[string]any{ "meta": map[string]any{"id": "h3", "reason": "thrash", "rotation_index": 0}, "anchor": map[string]any{"git_sha": head, "branch": "orchestra/t1"}, "goal": "g", "done_when": []string{"x"}, "action": "prompt", "command": "go test ./...", "dead_ends": []map[string]any{{"tried": "go test ./...", "why_failed": "failed 3 times"}}, } b, err := json.Marshal(handoff) if err != nil { t.Fatal(err) } handoffPath := repo + "/" + herdr.HandoffFile if err := os.WriteFile(handoffPath, b, 0o644); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Remove(handoffPath) }) decision, err := c.TurnDecision(context.Background(), task.ID) if err != nil { t.Fatal(err) } if decision != orchestrator.TurnRotateNow { t.Fatalf("decision=%q want %q", decision, orchestrator.TurnRotateNow) } if a.releases == 0 { t.Fatal("Release was never invoked for a written thrash handoff") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateQueued { t.Fatalf("task state=%v ok=%v, want queued", got.State, ok) } }) t.Run("rotate() also requests a reasoned handoff on thrash, without releasing", func(t *testing.T) { a := &activityAdapter{fakeAdapter: fakeAdapter{occupancy: 0, boundary: true}, calls: thrashCalls} c, st, task := newCoordinator(a) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) deadline := time.Now().Add(300 * time.Millisecond) for time.Now().Before(deadline) && len(a.reasonAsked) == 0 { time.Sleep(time.Millisecond) } if len(a.reasonAsked) == 0 || a.reasonAsked[0] != "thrash" { t.Fatalf("reasonAsked=%v want a thrash request from rotate()", a.reasonAsked) } if a.releases != 0 { t.Fatal("rotate() must not release on a bare thrash detection") } got, ok := st.Task(task.ID) if !ok || got.State != domain.StateLeased { t.Fatalf("task state=%v ok=%v, want still leased", got.State, ok) } }) } // TestRotationRequestsHandoffBeforeReleasing guards Phase 4 item 2 (AUDIT.md): // rotate() must not call Release until the agent has been told to write its // §6.1 handoff and the file actually exists — never invent or skip the ask. func TestRotationRequestsHandoffBeforeReleasing(t *testing.T) { repo := t.TempDir() run(t, repo, "init") run(t, repo, "config", "user.email", "t@t") run(t, repo, "config", "user.name", "t") run(t, repo, "commit", "--allow-empty", "-m", "init") s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] ref, err := s.PutArtifact([]byte("handoff")) if err != nil { t.Fatal(err) } a := &handoffRequestingAdapter{fakeAdapter: fakeAdapter{occupancy: .95, boundary: true, ref: ref}} c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) deadline := time.Now().Add(200 * time.Millisecond) for time.Now().Before(deadline) { time.Sleep(time.Millisecond) } if a.requests == 0 { t.Fatal("rotate never asked the agent to write a handoff") } if a.releases != 0 { t.Fatal("rotate called Release before the handoff file existed") } if got, ok := s.Task(task.ID); !ok || got.State != domain.StateLeased { t.Fatalf("task rotated without a handoff file: state=%v ok=%v", got.State, ok) } if err := os.WriteFile(repo+"/"+herdr.HandoffFile, []byte("{}"), 0644); err != nil { t.Fatal(err) } deadline = time.Now().Add(time.Second) for time.Now().Before(deadline) { if got, ok := s.Task(task.ID); ok && got.State == domain.StateQueued { break } time.Sleep(time.Millisecond) } if a.releases == 0 { t.Fatal("rotate never called Release once the handoff file appeared") } } type specWorktrees struct{ wtPath, repoPath string } func (w specWorktrees) Create(context.Context, domain.Task) (string, error) { return w.wtPath, nil } func (w specWorktrees) Spec(domain.Task) (string, string, bool) { return w.repoPath, "", true } type conventionsAdapter struct { fakeAdapter notifications int } func (a *conventionsAdapter) NotifyConventionsChanged(context.Context, herdr.Session) error { a.notifications++ return nil } // TestConventionsDriftNotifiesActiveSession guards §6.3's wiring: "on // update, the orchestra injects a notice to agents whose current task is // adjacent" — never left to the agent's own cached view. A session must not // be notified while the base repo's shared docs match what it started with, // and must be notified once they diverge. func TestConventionsDriftNotifiesActiveSession(t *testing.T) { repo := t.TempDir() worktree := t.TempDir() if err := os.WriteFile(repo+"/AGENTS.md", []byte("v1"), 0644); err != nil { t.Fatal(err) } if err := os.WriteFile(worktree+"/AGENTS.md", []byte("v1"), 0644); err != nil { t.Fatal(err) } s, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "t1", Surface: string(authz.System), Payload: mustJSON(map[string]any{ "source": "jsonl", "external_id": "1", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] a := &conventionsAdapter{fakeAdapter: fakeAdapter{occupancy: 0}} c := &orchestrator.Coordinator{Store: s, Worktrees: specWorktrees{wtPath: worktree, repoPath: repo}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"} leaseEvt, err := s.Lease(task.ID, "h1", time.Minute) if err != nil { t.Fatal(err) } if err := c.Start(context.Background(), leaseEvt); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go c.Monitor(ctx, .8, time.Millisecond) time.Sleep(50 * time.Millisecond) if a.notifications != 0 { t.Fatalf("notified with no actual drift: notifications=%d", a.notifications) } if err := os.WriteFile(repo+"/AGENTS.md", []byte("v2"), 0644); err != nil { t.Fatal(err) } deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) && a.notifications == 0 { time.Sleep(time.Millisecond) } if a.notifications == 0 { t.Fatal("session was never notified of the conventions-doc update") } }