Files
orchestra/internal/orchestrator/rotation_test.go
T
2026-07-30 14:37:34 +04:00

1044 lines
36 KiB
Go

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
leases int
approval struct {
called bool
grant bool
session herdr.Session
capture string
}
}
func (a *fakeAdapter) Lease(_ context.Context, _ string, worktree string) (herdr.Session, error) {
a.leases++
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
}
func (a *fakeAdapter) RespondApproval(_ context.Context, s herdr.Session, grant bool, capture string) error {
a.approval.called = true
a.approval.grant = grant
a.approval.session = s
a.approval.capture = capture
return 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 }
type promptFailureAdapter struct{ fakeAdapter }
func (a *promptFailureAdapter) LeasePrompt(_ context.Context, _ string, worktree, _ string) (herdr.Session, error) {
return herdr.Session{Harness: "h1", PaneID: "pane-created-before-timeout", Worktree: worktree}, errors.New("prompt delivery uncertain")
}
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)
}
}
func TestPromptFailureRetainsLivePaneForBlockedTaskAcrossRestart(t *testing.T) {
s, err := store.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "blocked-live-pane", Surface: string(authz.System), Payload: mustJSON(map[string]any{
"source": "qa", "external_id": "prompt-timeout", "project": "p",
})}); err != nil {
t.Fatal(err)
}
task, ok := s.Task("blocked-live-pane")
if !ok {
t.Fatal("created task missing")
}
lease, err := s.Lease(task.ID, "h1", time.Minute)
if err != nil {
t.Fatal(err)
}
statePath := t.TempDir() + "/sessions.json"
a := &promptFailureAdapter{}
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, StatePath: statePath}
if err := c.Start(context.Background(), lease); err != nil {
t.Fatal(err)
}
if got, ok := s.Task(task.ID); !ok || got.State != domain.StateNeedsAttention || got.Lease == nil {
t.Fatalf("task state = %+v, want needs_attention with retained lease", got)
}
if session, ok := c.Session(task.ID); !ok || session.PaneID != "pane-created-before-timeout" || session.HerdrID != "h1" {
t.Fatalf("retained session = %+v, present=%v", session, ok)
}
// A fresh coordinator must retain the mapping for a blocked task rather
// than treating it as an orphan after restart.
restarted := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, StatePath: statePath}
if err := restarted.Reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if session, ok := restarted.Session(task.ID); !ok || session.PaneID != "pane-created-before-timeout" {
t.Fatalf("restarted session = %+v, present=%v", session, ok)
}
}
func TestRespondApprovalUsesOwningSessionAndPreservesCaptureBinding(t *testing.T) {
s, err := store.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "approval-task", Surface: string(authz.System), Payload: mustJSON(map[string]any{
"source": "qa", "external_id": "approval", "project": "p",
})}); err != nil {
t.Fatal(err)
}
lease, err := s.Lease("approval-task", "herdr-1", time.Minute)
if err != nil {
t.Fatal(err)
}
a := &fakeAdapter{}
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, StatePath: t.TempDir() + "/sessions.json"}
if err := c.Start(context.Background(), lease); err != nil {
t.Fatal(err)
}
const capture = "Approval required\n$ go test ./...\n[y/n]"
if err := c.RespondApproval(context.Background(), "approval-task", true, capture); err != nil {
t.Fatal(err)
}
if !a.approval.called || !a.approval.grant || a.approval.capture != capture {
t.Fatalf("approval invocation = %#v", a.approval)
}
if a.approval.session.HerdrID != "herdr-1" || a.approval.session.PaneID == "" {
t.Fatalf("approval used wrong session: %#v", a.approval.session)
}
}
func TestCoordinatorRefusesRemoteHerdrOperations(t *testing.T) {
s, err := store.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
if err := s.Append(domain.Event{ID: domain.NewID(), Type: "TaskCreated", TaskID: "remote", Surface: string(authz.System), Payload: mustJSON(map[string]any{
"source": "qa", "external_id": "remote", "project": "p",
})}); err != nil {
t.Fatal(err)
}
lease, err := s.Lease("remote", "remote", time.Minute)
if err != nil {
t.Fatal(err)
}
a := &fakeAdapter{}
c := &orchestrator.Coordinator{Store: s, Worktrees: worktrees{path: t.TempDir()}, Adapters: adapters{a}, LocalHerdr: func(id string) bool { return id == "local" }}
if err := c.Start(context.Background(), lease); err != nil {
t.Fatal(err)
}
if a.leases != 0 {
t.Fatal("remote adapter was started by coordinator")
}
if task, ok := s.Task("remote"); !ok || task.State != domain.StateNeedsAttention || task.Lease == nil {
t.Fatalf("remote task state = %#v, present=%v; want needs_attention with retained lease", task, ok)
}
}
// 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"},
"action": "run the focused tests", "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
// (TaskNeedsAttention) 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"},
"action": "run the focused tests", "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.StateNeedsAttention || got.Lease == nil {
t.Fatalf("expected recoverable needs_attention on invalid pickup, got state=%v lease=%v ok=%v", got.State, got.Lease, 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
observed chan struct{}
}
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
if a.observed != nil {
select {
case a.observed <- struct{}{}:
default:
}
}
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, observed: make(chan struct{}, 1)}
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"},
"action": "run the focused tests", "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())
done := make(chan error, 1)
go func() { done <- c.Monitor(ctx, .8, time.Millisecond) }()
select {
case <-a.observed:
case <-time.After(300 * time.Millisecond):
}
cancel()
<-done
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.HandoffReportFile, []byte("handoff evidence"), 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
observed chan struct{}
}
func (a *conventionsAdapter) NotifyConventionsChanged(context.Context, herdr.Session) error {
a.notifications++
if a.observed != nil {
select {
case a.observed <- struct{}{}:
default:
}
}
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}, observed: make(chan struct{}, 1)}
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())
done := make(chan error, 1)
go func() { done <- 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)
}
select {
case <-a.observed:
case <-time.After(time.Second):
}
cancel()
<-done
if a.notifications == 0 {
t.Fatal("session was never notified of the conventions-doc update")
}
}