package orch import ( "os" "path/filepath" "testing" ) func TestOrchRunInitCreatesNewRun(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") initOut := runOrchCommand( t, "--db", dbPath, "--json", "run", "init", "--run", "run_blog_init_001", "--goal", "Build blog MVP", "--summary", "Public blog plus admin CRUD", ) var initResp map[string]any mustDecodeJSON(t, initOut, &initResp) if got := nestedString(t, initResp, "data", "run", "run_id"); got != "run_blog_init_001" { t.Fatalf("expected run id run_blog_init_001, got %q", got) } if got := nestedString(t, initResp, "data", "run", "goal"); got != "Build blog MVP" { t.Fatalf("expected goal Build blog MVP, got %q", got) } if got := nestedString(t, initResp, "data", "run", "summary"); got != "Public blog plus admin CRUD" { t.Fatalf("expected summary to round-trip, got %q", got) } if got := nestedString(t, initResp, "data", "run", "status"); got != "active" { t.Fatalf("expected new run status active, got %q", got) } assertNonEmptyNestedString(t, initResp, "data", "run", "created_at") assertNonEmptyNestedString(t, initResp, "data", "run", "updated_at") showOut := runOrchCommand( t, "--db", dbPath, "--json", "run", "show", "--run", "run_blog_init_001", ) var showResp map[string]any mustDecodeJSON(t, showOut, &showResp) if got := nestedString(t, showResp, "data", "run", "run_id"); got != "run_blog_init_001" { t.Fatalf("expected persisted run id run_blog_init_001, got %q", got) } if got := nestedString(t, showResp, "data", "run", "status"); got != "active" { t.Fatalf("expected persisted run status active, got %q", got) } } func TestOrchDispatchCreatesAttemptAndThreadForReadyTask(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") runOrchCommand( t, "--db", dbPath, "--json", "run", "init", "--run", "run_blog_dispatch_001", "--goal", "Build blog MVP", "--summary", "Public blog plus admin CRUD", ) runOrchCommand( t, "--db", dbPath, "--json", "task", "add", "--run", "run_blog_dispatch_001", "--task", "T1", "--title", "Implement retry policy", "--summary", "Add retry policy to HTTP client", "--default-to", "worker-a", ) dispatchOut := runOrchCommand( t, "--db", dbPath, "--json", "dispatch", "--run", "run_blog_dispatch_001", "--task", "T1", "--body", "Implement retry handling for the HTTP client.", ) var dispatchResp map[string]any mustDecodeJSON(t, dispatchOut, &dispatchResp) if got := nestedString(t, dispatchResp, "data", "task", "status"); got != "dispatched" { t.Fatalf("expected dispatched task status, got %q", got) } if got := nestedValue(t, dispatchResp, "data", "attempt", "attempt_no").(float64); got != 1 { t.Fatalf("expected attempt_no 1, got %#v", got) } threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id") if threadID == "" { t.Fatal("expected non-empty attempt thread_id") } if got := nestedString(t, dispatchResp, "data", "attempt", "assigned_to"); got != "worker-a" { t.Fatalf("expected assigned_to worker-a, got %q", got) } if got := nestedString(t, dispatchResp, "data", "thread", "thread_id"); got != threadID { t.Fatalf("expected thread.thread_id %q, got %q", threadID, got) } if got := nestedString(t, dispatchResp, "data", "message", "kind"); got != "task" { t.Fatalf("expected first dispatch message kind task, got %q", got) } } func TestOrchBlockedListsLatestQuestionForBlockedTask(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") runOrchCommand( t, "--db", dbPath, "--json", "run", "init", "--run", "run_blog_blocked_001", "--goal", "Build dependency-aware workflow", ) runOrchCommand( t, "--db", dbPath, "--json", "task", "add", "--run", "run_blog_blocked_001", "--task", "T1", "--title", "Build backend", "--summary", "Implement backend APIs", "--default-to", "worker-a", ) runOrchCommand( t, "--db", dbPath, "--json", "task", "add", "--run", "run_blog_blocked_001", "--task", "T2", "--title", "Build frontend", "--summary", "Implement frontend flows", "--default-to", "worker-b", ) runOrchCommand( t, "--db", dbPath, "--json", "dep", "add", "--run", "run_blog_blocked_001", "--task", "T2", "--depends-on", "T1", ) firstDispatch := runOrchCommand( t, "--db", dbPath, "--json", "dispatch", "--run", "run_blog_blocked_001", "--task", "T1", ) var firstDispatchResp map[string]any mustDecodeJSON(t, firstDispatch, &firstDispatchResp) threadBackend := nestedString(t, firstDispatchResp, "data", "attempt", "thread_id") runInboxCommand( t, "--db", dbPath, "--json", "claim", "--agent", "worker-a", "--thread", threadBackend, ) runInboxCommand( t, "--db", dbPath, "--json", "done", "--agent", "worker-a", "--thread", threadBackend, "--summary", "Backend complete", ) runOrchCommand( t, "--db", dbPath, "--json", "reconcile", "--run", "run_blog_blocked_001", ) secondDispatch := runOrchCommand( t, "--db", dbPath, "--json", "dispatch", "--run", "run_blog_blocked_001", "--task", "T2", ) var secondDispatchResp map[string]any mustDecodeJSON(t, secondDispatch, &secondDispatchResp) threadFrontend := nestedString(t, secondDispatchResp, "data", "attempt", "thread_id") runInboxCommand( t, "--db", dbPath, "--json", "claim", "--agent", "worker-b", "--thread", threadFrontend, ) runInboxCommand( t, "--db", dbPath, "--json", "update", "--agent", "worker-b", "--thread", threadFrontend, "--status", "blocked", "--summary", "Need logging decision", "--payload-json", `{"question":"stdout or stderr?"}`, ) runOrchCommand( t, "--db", dbPath, "--json", "reconcile", "--run", "run_blog_blocked_001", ) blockedOut := runOrchCommand( t, "--db", dbPath, "--json", "blocked", "--run", "run_blog_blocked_001", ) var blockedResp map[string]any mustDecodeJSON(t, blockedOut, &blockedResp) blockedTasks := nestedArray(t, blockedResp, "data", "blocked") if len(blockedTasks) != 1 { t.Fatalf("expected one blocked task, got %#v", blockedTasks) } blockedTask, ok := blockedTasks[0].(map[string]any) if !ok { t.Fatalf("expected blocked task object, got %#v", blockedTasks[0]) } if got := nestedString(t, blockedTask, "task", "task_id"); got != "T2" { t.Fatalf("expected blocked task T2, got %q", got) } if got := nestedString(t, blockedTask, "question", "kind"); got != "question" { t.Fatalf("expected question.kind=question, got %q", got) } if got := nestedString(t, blockedTask, "question", "summary"); got != "Need logging decision" { t.Fatalf("expected question summary to match latest blocked message, got %q", got) } questionPayload, ok := nestedValue(t, blockedTask, "question", "payload_json").(map[string]any) if !ok { t.Fatalf("expected question payload_json object, got %#v", nestedValue(t, blockedTask, "question", "payload_json")) } if got, _ := questionPayload["question"].(string); got != "stdout or stderr?" { t.Fatalf("expected latest question payload, got %#v", questionPayload["question"]) } } func TestOrchStatusReturnsRunSummaryAndTaskList(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") runOrchCommand( t, "--db", dbPath, "--json", "run", "init", "--run", "run_blog_status_001", "--goal", "Build blog MVP", ) runOrchCommand( t, "--db", dbPath, "--json", "task", "add", "--run", "run_blog_status_001", "--task", "T1", "--title", "Implement retry policy", "--default-to", "worker-a", ) dispatchOut := runOrchCommand( t, "--db", dbPath, "--json", "dispatch", "--run", "run_blog_status_001", "--task", "T1", "--body", "Implement retry handling for the HTTP client.", ) var dispatchResp map[string]any mustDecodeJSON(t, dispatchOut, &dispatchResp) threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id") runInboxCommand( t, "--db", dbPath, "--json", "claim", "--agent", "worker-a", "--thread", threadID, ) runInboxCommand( t, "--db", dbPath, "--json", "done", "--agent", "worker-a", "--thread", threadID, "--summary", "Retry policy implemented", "--body", "The HTTP client now retries transient failures.", ) runOrchCommand( t, "--db", dbPath, "--json", "reconcile", "--run", "run_blog_status_001", ) statusOut := runOrchCommand( t, "--db", dbPath, "--json", "status", "--run", "run_blog_status_001", ) var statusResp map[string]any mustDecodeJSON(t, statusOut, &statusResp) if got := nestedString(t, statusResp, "data", "run", "run_id"); got != "run_blog_status_001" { t.Fatalf("expected run_id run_blog_status_001, got %q", got) } if got := nestedString(t, statusResp, "data", "run", "status"); got != "done" { t.Fatalf("expected run status done, got %q", got) } taskCounts, ok := nestedValue(t, statusResp, "data", "task_counts").(map[string]any) if !ok { t.Fatalf("expected task_counts object, got %#v", nestedValue(t, statusResp, "data", "task_counts")) } if got, _ := taskCounts["done"].(float64); got != 1 { t.Fatalf("expected done task count 1, got %#v", taskCounts["done"]) } tasks := nestedArray(t, statusResp, "data", "tasks") if len(tasks) != 1 { t.Fatalf("expected one task in status response, got %#v", tasks) } task, ok := tasks[0].(map[string]any) if !ok { t.Fatalf("expected task object, got %#v", tasks[0]) } if got, _ := task["task_id"].(string); got != "T1" { t.Fatalf("expected task_id T1, got %#v", task["task_id"]) } if got, _ := task["status"].(string); got != "done" { t.Fatalf("expected task status done, got %#v", task["status"]) } } func TestOrchReconcileMapsFailedThreadToTerminalTaskState(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") runOrchCommand( t, "--db", dbPath, "--json", "run", "init", "--run", "run_blog_reconcile_001", "--goal", "Build blog MVP", ) runOrchCommand( t, "--db", dbPath, "--json", "task", "add", "--run", "run_blog_reconcile_001", "--task", "T1", "--title", "Implement retry policy", "--default-to", "worker-a", ) dispatchOut := runOrchCommand( t, "--db", dbPath, "--json", "dispatch", "--run", "run_blog_reconcile_001", "--task", "T1", "--body", "Implement retry handling for the HTTP client.", ) var dispatchResp map[string]any mustDecodeJSON(t, dispatchOut, &dispatchResp) threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id") runInboxCommand( t, "--db", dbPath, "--json", "claim", "--agent", "worker-a", "--thread", threadID, ) runInboxCommand( t, "--db", dbPath, "--json", "fail", "--agent", "worker-a", "--thread", threadID, "--summary", "Retry policy failed", "--body", "The HTTP client kept failing integration tests.", ) reconcileOut := runOrchCommand( t, "--db", dbPath, "--json", "reconcile", "--run", "run_blog_reconcile_001", ) var reconcileResp map[string]any mustDecodeJSON(t, reconcileOut, &reconcileResp) updatedTasks := nestedArray(t, reconcileResp, "data", "updated_tasks") if len(updatedTasks) != 1 { t.Fatalf("expected one updated task after failed reconcile, got %#v", updatedTasks) } task, ok := updatedTasks[0].(map[string]any) if !ok { t.Fatalf("expected updated task object, got %#v", updatedTasks[0]) } if got, _ := task["task_id"].(string); got != "T1" { t.Fatalf("expected updated task T1, got %#v", task["task_id"]) } if got, _ := task["status"].(string); got != "failed" { t.Fatalf("expected reconciled task status failed, got %#v", task["status"]) } taskCounts, ok := nestedValue(t, reconcileResp, "data", "task_counts").(map[string]any) if !ok { t.Fatalf("expected task_counts object, got %#v", nestedValue(t, reconcileResp, "data", "task_counts")) } if got, _ := taskCounts["failed"].(float64); got != 1 { t.Fatalf("expected failed task count 1 after reconcile, got %#v", taskCounts["failed"]) } statusOut := runOrchCommand( t, "--db", dbPath, "--json", "status", "--run", "run_blog_reconcile_001", ) var statusResp map[string]any mustDecodeJSON(t, statusOut, &statusResp) if got := nestedString(t, statusResp, "data", "run", "status"); got != "failed" { t.Fatalf("expected run status failed after failed reconcile, got %q", got) } } func TestOrchWorkflowStrictWorktreeDispatchToCleanup(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") repoPath := initGitRepo(t) runOrchCommand( t, "--db", dbPath, "--json", "run", "init", "--run", "run_blog_workflow_worktree_001", "--goal", "Validate strict worktree dispatch", ) runOrchCommand( t, "--db", dbPath, "--json", "task", "add", "--run", "run_blog_workflow_worktree_001", "--task", "T1", "--title", "Implement backend", "--default-to", "worker-a", ) dispatchOut := runOrchCommand( t, "--db", dbPath, "--json", "dispatch", "--run", "run_blog_workflow_worktree_001", "--task", "T1", "--repo-path", repoPath, "--workspace-root", ".orch/worktrees", "--strict-worktree", "--body", "Implement inside isolated worktree.", ) var dispatchResp map[string]any mustDecodeJSON(t, dispatchOut, &dispatchResp) threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id") worktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path") if worktreePath == "" { t.Fatal("expected non-empty worktree_path for strict worktree workflow") } if got := nestedString(t, dispatchResp, "data", "attempt", "workspace_status"); got != "created" { t.Fatalf("expected workspace_status created, got %q", got) } runInboxCommand( t, "--db", dbPath, "--json", "claim", "--agent", "worker-a", "--thread", threadID, ) runInboxCommand( t, "--db", dbPath, "--json", "done", "--agent", "worker-a", "--thread", threadID, "--summary", "Backend complete", ) runOrchCommand( t, "--db", dbPath, "--json", "reconcile", "--run", "run_blog_workflow_worktree_001", ) cleanupOut := runOrchCommand( t, "--db", dbPath, "--json", "cleanup", "--run", "run_blog_workflow_worktree_001", "--task", "T1", "--attempt", "1", ) var cleanupResp map[string]any mustDecodeJSON(t, cleanupOut, &cleanupResp) cleaned := nestedArray(t, cleanupResp, "data", "cleaned") if len(cleaned) != 1 { t.Fatalf("expected one cleaned attempt, got %#v", cleaned) } cleanedAttempt, ok := cleaned[0].(map[string]any) if !ok { t.Fatalf("expected cleaned attempt object, got %#v", cleaned[0]) } if got, _ := cleanedAttempt["workspace_status"].(string); got != "cleaned" { t.Fatalf("expected cleaned workspace_status, got %#v", cleanedAttempt["workspace_status"]) } if _, err := os.Stat(worktreePath); !os.IsNotExist(err) { t.Fatalf("expected cleaned worktree path to be removed, err=%v", err) } } func TestOrchWorkflowCouncilReviewEndToEnd(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "coord.db") runID := "council_blog_workflow_001" startOut := runOrchCommand( t, "--db", dbPath, "--json", "council", "start", "--run", runID, "--target", "Review the current blog architecture.", ) var startResp map[string]any mustDecodeJSON(t, startOut, &startResp) reviewers := nestedArray(t, startResp, "data", "reviewers") if len(reviewers) != 3 { t.Fatalf("expected three reviewers from council start, got %#v", reviewers) } completeCouncilWorkflowReviewersForRemainingTests(t, dbPath, runID) waitOut := runOrchCommand( t, "--db", dbPath, "--json", "council", "wait", "--run", runID, "--timeout-seconds", "2", ) var waitResp map[string]any mustDecodeJSON(t, waitOut, &waitResp) if woke, _ := nestedValue(t, waitResp, "data", "woke").(bool); !woke { t.Fatalf("expected council wait to wake, got %#v", waitResp) } if allComplete, _ := nestedValue(t, waitResp, "data", "all_complete").(bool); !allComplete { t.Fatalf("expected all reviewers complete, got %#v", waitResp) } tallyOut := runOrchCommand( t, "--db", dbPath, "--json", "council", "tally", "--run", runID, "--similarity", "normal", ) var tallyResp map[string]any mustDecodeJSON(t, tallyOut, &tallyResp) if got := nestedString(t, tallyResp, "data", "similarity"); got != "normal" { t.Fatalf("expected normal tally similarity, got %q", got) } tallyCounts, ok := nestedValue(t, tallyResp, "data", "counts").(map[string]any) if !ok { t.Fatalf("expected tally counts object, got %#v", nestedValue(t, tallyResp, "data", "counts")) } if got, _ := tallyCounts["consensus"].(float64); got != 1 { t.Fatalf("expected one consensus group, got %#v", tallyCounts["consensus"]) } if got, _ := tallyCounts["majority"].(float64); got != 1 { t.Fatalf("expected one majority group, got %#v", tallyCounts["majority"]) } if got, _ := tallyCounts["minority"].(float64); got != 1 { t.Fatalf("expected one minority group, got %#v", tallyCounts["minority"]) } reportOut := runOrchCommand( t, "--db", dbPath, "--json", "council", "report", "--run", runID, ) var reportResp map[string]any mustDecodeJSON(t, reportOut, &reportResp) show := nestedArray(t, reportResp, "data", "show") if len(show) != 2 || show[0] != "consensus" || show[1] != "majority" { t.Fatalf("expected default report show [consensus majority], got %#v", show) } grouped := nestedArray(t, reportResp, "data", "grouped_recommendations") if len(grouped) != 2 { t.Fatalf("expected default report to include consensus and majority groups, got %#v", grouped) } artifacts := nestedArray(t, reportResp, "data", "report_artifacts") if len(artifacts) != 1 { t.Fatalf("expected one report artifact, got %#v", artifacts) } artifact, ok := artifacts[0].(map[string]any) if !ok { t.Fatalf("expected report artifact object, got %#v", artifacts[0]) } reportPath, _ := artifact["path"].(string) if reportPath == "" { t.Fatalf("expected report artifact path, got %#v", artifact["path"]) } if _, err := os.Stat(reportPath); err != nil { t.Fatalf("expected report artifact to exist at %q: %v", reportPath, err) } } func assertNonEmptyNestedString(t *testing.T, value map[string]any, keys ...string) { t.Helper() if got := nestedString(t, value, keys...); got == "" { t.Fatalf("expected non-empty string at %v", keys) } } func completeCouncilWorkflowReviewersForRemainingTests(t *testing.T, dbPath, runID string) { t.Helper() completeCouncilReviewer( t, dbPath, runID, "architecture-reviewer", `{"reviewer_role":"architecture-reviewer","findings":[{"title":"Split contracts","summary":"Transport contracts are mixed into UI code.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This lowers coupling.","confidence":"high","tags":["architecture"],"target_refs":{"repo_path":"."}},{"title":"Share helpers","summary":"Council report rendering paths are repeated.","proposal":"Introduce shared council coordinator helpers for report rendering.","rationale":"This keeps report assembly consistent.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`, ) completeCouncilReviewer( t, dbPath, runID, "implementation-reviewer", `{"reviewer_role":"implementation-reviewer","findings":[{"title":"Extract contracts","summary":"Shared transport shapes are duplicated.","proposal":"Move API contract definitions into dedicated module","rationale":"This reduces duplication.","confidence":"high","tags":["maintainability"],"target_refs":{"repo_path":"."}},{"title":"Reuse report helpers","summary":"Formatting logic should stay shared.","proposal":"Introduce shared council coordinator helpers for report rendering","rationale":"This avoids formatter drift.","confidence":"medium","tags":["reporting"],"target_refs":{"repo_path":"."}}]}`, ) completeCouncilReviewer( t, dbPath, runID, "risk-reviewer", `{"reviewer_role":"risk-reviewer","findings":[{"title":"Lock contracts","summary":"Contract drift becomes risky over time.","proposal":"Move API contract definitions into a dedicated module.","rationale":"This reduces integration regressions.","confidence":"high","tags":["risk"],"target_refs":{"repo_path":"."}},{"title":"Cover JSON output","summary":"The council report response should stay stable.","proposal":"Add regression tests for council report JSON output.","rationale":"This catches contract regressions earlier.","confidence":"high","tags":["testing"],"target_refs":{"repo_path":"."}}]}`, ) }