package orchestrator_test import ( "context" "errors" "strings" "testing" "time" "orchestra/internal/authz" "orchestra/internal/domain" "orchestra/internal/orchestrator" "orchestra/internal/store" ) func remoteLeased(t *testing.T) (*orchestrator.Coordinator, *store.Store, domain.Task) { t.Helper() 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": "gitea", "external_id": "381", "project": "p", })}); err != nil { t.Fatal(err) } task := s.Tasks()[0] if _, err := s.Lease(task.ID, "workpc-opencode", time.Minute); err != nil { t.Fatal(err) } // No Worktrees and no Adapters: the coordinator never touches a remote pane. c := &orchestrator.Coordinator{Store: s, StatePath: t.TempDir() + "/sessions.json"} got, _ := s.Task(task.ID) return c, s, got } // A worker at a verified boundary reconciles through the coordinator and gets // the decisions its session has not seen. func TestRemoteTurnReconcilesAndReturnsUndeliveredDecisions(t *testing.T) { c, s, task := remoteLeased(t) reconciled := 0 c.ReconcileHumanInput = func(_ context.Context, taskID string) error { reconciled++ if reconciled == 1 { recordDecision(t, s, taskID, "d1", "no, use b") } return nil } verdict, decisions, err := c.RemoteTurn(context.Background(), task.ID, task.Lease.Epoch, orchestrator.TurnContinue, nil) if err != nil { t.Fatal(err) } if verdict != orchestrator.TurnContinue { t.Fatalf("verdict = %q", verdict) } if len(decisions) != 1 || decisions[0].Value != "no, use b" { t.Fatalf("decisions = %+v", decisions) } // Delivered once. The worker reports what it has shown, so the same // decision is not returned twice. _, again, err := c.RemoteTurn(context.Background(), task.ID, task.Lease.Epoch, orchestrator.TurnContinue, []string{decisions[0].ID}) if err != nil { t.Fatal(err) } if len(again) != 0 { t.Fatalf("decision returned twice: %+v", again) } } // Rotating sessions get no decisions: the successor picks them up at re-lease. func TestRemoteTurnWithholdsDecisionsWhenRotating(t *testing.T) { c, s, task := remoteLeased(t) once := 0 c.ReconcileHumanInput = func(_ context.Context, taskID string) error { once++ if once == 1 { recordDecision(t, s, taskID, "d1", "no, use b") } return nil } for _, verdict := range []string{orchestrator.TurnRotateNow, orchestrator.TurnPrepareHandoff, orchestrator.TurnRefuse} { got, decisions, err := c.RemoteTurn(context.Background(), task.ID, task.Lease.Epoch, verdict, nil) if err != nil { t.Fatal(err) } if got != verdict || len(decisions) != 0 { t.Fatalf("verdict %q returned %+v", verdict, decisions) } } } // Fenced like every other worker-driven call. func TestRemoteTurnRefusesStaleEpoch(t *testing.T) { c, _, task := remoteLeased(t) if _, _, err := c.RemoteTurn(context.Background(), task.ID, "stale", orchestrator.TurnContinue, nil); !errors.Is(err, domain.ErrConflict) { t.Fatalf("want ErrConflict, got %v", err) } if _, _, err := c.RemoteTurn(context.Background(), "missing", "e", orchestrator.TurnContinue, nil); !errors.Is(err, domain.ErrNotFound) { t.Fatalf("want ErrNotFound, got %v", err) } } // Same contract as the local boundary: a source failure is observable and the // session keeps running. func TestRemoteTurnReconcileFailureIsObservableNotFatal(t *testing.T) { c, _, task := remoteLeased(t) c.ReconcileHumanInput = func(context.Context, string) error { return context.DeadlineExceeded } verdict, _, err := c.RemoteTurn(context.Background(), task.ID, task.Lease.Epoch, orchestrator.TurnContinue, nil) if err != nil { t.Fatal(err) } if verdict != orchestrator.TurnContinue { t.Fatalf("verdict = %q", verdict) } h := c.MonitorHealth().Sessions[task.ID] if !strings.Contains(h.LastError, "reconcile human input") { t.Fatalf("failure not observable: %+v", h) } }