Add inbox and orch command contract tests

This commit is contained in:
2026-03-24 09:06:30 +08:00
parent fd2b57feaf
commit ea3b617ad1
12 changed files with 2088 additions and 9 deletions
@@ -0,0 +1,103 @@
package inbox
import (
"os"
"path/filepath"
"testing"
)
// TestReplyReadsBodyFromBodyFile verifies reply loads its message body from a body file.
func TestReplyReadsBodyFromBodyFile(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
bodyPath := filepath.Join(tempDir, "reply.md")
body := "Decision note from body file."
if err := os.WriteFile(bodyPath, []byte(body), 0o644); err != nil {
t.Fatalf("write reply body file: %v", err)
}
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Use the retry policy",
"--body-file", bodyPath,
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
if got := nestedString(t, replyResp, "data", "message", "body"); got != body {
t.Fatalf("expected reply body %q, got %q", body, got)
}
}
// TestReplyPersistsPayloadJSON verifies reply preserves payload JSON on a successful reply.
func TestReplyPersistsPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Retry read timeouts",
"--payload-json", `{"decision":"retry","owner":"leader"}`,
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
payload, ok := nestedValue(t, replyResp, "data", "message", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, replyResp, "data", "message", "payload_json"))
}
if got := payload["decision"]; got != "retry" {
t.Fatalf("expected decision retry, got %#v", got)
}
if got := payload["owner"]; got != "leader" {
t.Fatalf("expected owner leader, got %#v", got)
}
}
// TestReplyRejectsInvalidArtifactMetadataJSON verifies reply rejects malformed artifact metadata JSON.
func TestReplyRejectsInvalidArtifactMetadataJSON(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
artifactPath := filepath.Join(tempDir, "decision.md")
if err := os.WriteFile(artifactPath, []byte("Decision note."), 0o644); err != nil {
t.Fatalf("write artifact file: %v", err)
}
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--summary", "Retry read timeouts",
"--artifact", artifactPath,
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
@@ -0,0 +1,102 @@
package inbox
import (
"path/filepath"
"testing"
)
// TestSendSupportsExplicitKindPriorityAndPayload verifies send honors explicit kind, priority, and payload JSON.
func TestSendSupportsExplicitKindPriorityAndPayload(t *testing.T) {
t.Parallel()
dbPath := initCommandTestDB(t)
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-a",
"--subject", "Clarify auth behavior",
"--summary", "Need redirect policy",
"--kind", "question",
"--priority", "high",
"--payload-json", `{"topic":"auth","severity":"high"}`,
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
if got := nestedString(t, sendResp, "data", "message", "kind"); got != "question" {
t.Fatalf("expected message kind question, got %q", got)
}
if got := nestedString(t, sendResp, "data", "thread", "priority"); got != "high" {
t.Fatalf("expected thread priority high, got %q", got)
}
payload, ok := nestedValue(t, sendResp, "data", "message", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, sendResp, "data", "message", "payload_json"))
}
if got, _ := payload["topic"].(string); got != "auth" {
t.Fatalf("expected payload topic auth, got %#v", payload["topic"])
}
if got, _ := payload["severity"].(string); got != "high" {
t.Fatalf("expected payload severity high, got %#v", payload["severity"])
}
}
// TestReplySupportsQuestionAndProgressKinds verifies reply accepts the remaining documented non-default kinds.
func TestReplySupportsQuestionAndProgressKinds(t *testing.T) {
t.Parallel()
cases := []struct {
name string
kind string
summary string
body string
}{
{
name: "question",
kind: "question",
summary: "Need product confirmation",
body: "Should the guest redirect happen before the paywall?",
},
{
name: "progress",
kind: "progress",
summary: "Investigating",
body: "Checking the redirect and onboarding flows.",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
replyOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-a",
"--thread", threadID,
"--kind", tc.kind,
"--summary", tc.summary,
"--body", tc.body,
)
var replyResp map[string]any
mustDecodeJSON(t, replyOut, &replyResp)
if got := nestedString(t, replyResp, "data", "message", "kind"); got != tc.kind {
t.Fatalf("expected reply kind %q, got %q", tc.kind, got)
}
if got := nestedString(t, replyResp, "data", "message", "summary"); got != tc.summary {
t.Fatalf("expected reply summary %q, got %q", tc.summary, got)
}
})
}
}
@@ -0,0 +1,165 @@
package inbox
import (
"path/filepath"
"testing"
)
// TestDonePersistsPayloadJSONOnFinalMessage verifies done persists payload JSON on the final result message.
func TestDonePersistsPayloadJSONOnFinalMessage(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--payload-json", `{"artifact":"report","lines":12}`,
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
lastMessage := lastThreadMessageFromShow(t, showResp)
payload, ok := lastMessage["payload_json"].(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", lastMessage["payload_json"])
}
if got, _ := payload["artifact"].(string); got != "report" {
t.Fatalf("expected payload artifact report, got %#v", payload["artifact"])
}
if got, _ := payload["lines"].(float64); got != 12 {
t.Fatalf("expected payload lines 12, got %#v", payload["lines"])
}
}
// TestFailPersistsPayloadJSONOnFailureMessage verifies fail persists payload JSON on the failure result message.
func TestFailPersistsPayloadJSONOnFailureMessage(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
"--payload-json", `{"code":"schema_mismatch","retryable":false}`,
)
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
lastMessage := lastThreadMessageFromShow(t, showResp)
payload, ok := lastMessage["payload_json"].(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", lastMessage["payload_json"])
}
if got, _ := payload["code"].(string); got != "schema_mismatch" {
t.Fatalf("expected payload code schema_mismatch, got %#v", payload["code"])
}
if got, _ := payload["retryable"].(bool); got {
t.Fatalf("expected retryable=false, got %#v", payload["retryable"])
}
}
// TestDoneRejectsInvalidArtifactMetadataJSONInput verifies done rejects malformed artifact metadata JSON.
func TestDoneRejectsInvalidArtifactMetadataJSONInput(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
artifactPath := filepath.Join(tempDir, "result.md")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--artifact", artifactPath,
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestFailRejectsInvalidArtifactMetadataJSONInput verifies fail rejects malformed artifact metadata JSON.
func TestFailRejectsInvalidArtifactMetadataJSONInput(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
artifactPath := filepath.Join(tempDir, "failure.md")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
"--artifact", artifactPath,
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestCancelRejectsInvalidArtifactMetadataJSONInput verifies cancel rejects malformed artifact metadata JSON.
func TestCancelRejectsInvalidArtifactMetadataJSONInput(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
threadID := seedThreadForInboxTests(t, dbPath, "leader", "worker-a")
artifactPath := filepath.Join(tempDir, "cancel.md")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"cancel",
"--agent", "leader",
"--thread", threadID,
"--reason", "Task superseded by a larger refactor",
"--artifact", artifactPath,
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
@@ -0,0 +1,72 @@
package inbox
import (
"path/filepath"
"testing"
)
// TestUpdateRejectsInvalidStatus verifies update rejects statuses outside the documented contract.
func TestUpdateRejectsInvalidStatus(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "pending",
"--summary", "Unexpected status",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestDoneRejectsInvalidPayloadJSON verifies done rejects malformed payload JSON.
func TestDoneRejectsInvalidPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Retry policy implemented",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestFailRejectsInvalidPayloadJSON verifies fail rejects malformed payload JSON.
func TestFailRejectsInvalidPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-b", "worker-b")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-b",
"--thread", threadID,
"--summary", "Migration failed",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
@@ -0,0 +1,98 @@
package inbox
import (
"path/filepath"
"testing"
"time"
)
// TestWatchWakesOnTerminalStatuses verifies watch can wake on done and failed status filters.
func TestWatchWakesOnTerminalStatuses(t *testing.T) {
cases := []struct {
name string
status string
agent string
finish func(t *testing.T, dbPath, threadID, agent string)
}{
{
name: "done",
status: "done",
agent: "worker-d",
finish: func(t *testing.T, dbPath, threadID, agent string) {
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", agent,
"--thread", threadID,
"--summary", "Task complete",
)
},
},
{
name: "failed",
status: "failed",
agent: "worker-f",
finish: func(t *testing.T, dbPath, threadID, agent string) {
runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", agent,
"--thread", threadID,
"--summary", "Task failed",
)
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "coord.db")
runInboxCommand(t, "--db", dbPath, "--json", "init")
threadID := sendPendingThread(t, dbPath, "leader", tc.agent, "Implement feature", "Initial request")
watchCh := make(chan watchCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
"--agent", tc.agent,
"--status", tc.status,
"--timeout-seconds", "2",
)
watchCh <- watchCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", tc.agent,
"--thread", threadID,
)
tc.finish(t, dbPath, threadID, tc.agent)
select {
case result := <-watchCh:
if result.exit != 0 {
t.Fatalf("watch failed with exit=%d\nstderr:\n%s\nstdout:\n%s", result.exit, result.stderr, result.stdout)
}
var watchResp map[string]any
mustDecodeJSON(t, result.stdout, &watchResp)
if got := nestedString(t, watchResp, "data", "thread", "status"); got != tc.status {
t.Fatalf("expected watch status %q, got %q", tc.status, got)
}
case <-time.After(3 * time.Second):
t.Fatalf("watch command did not return for terminal status %s", tc.status)
}
})
}
}
@@ -0,0 +1,159 @@
package inbox
import (
"path/filepath"
"testing"
"time"
)
// TestUpdateRejectsInvalidArtifactMetadataJSONOnProgressUpdate verifies update rejects malformed artifact metadata JSON on progress updates.
func TestUpdateRejectsInvalidArtifactMetadataJSONOnProgressUpdate(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
artifactPath := filepath.Join(tempDir, "progress.md")
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", threadID,
"--status", "in_progress",
"--summary", "Implementation started",
"--artifact", artifactPath,
"--artifact-metadata-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d with output %s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestWaitReplyWakesOnResultKindFilter verifies wait-reply wakes on a result message when --kinds result is used.
func TestWaitReplyWakesOnResultKindFilter(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID, blockedMessageID := seedBlockedThreadForWaitReply(t, dbPath)
waitCh := make(chan waitReplyCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--after-message", blockedMessageID,
"--kinds", "result",
"--timeout-seconds", "2",
)
waitCh <- waitReplyCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-c",
"--thread", threadID,
"--summary", "Worker completed after unblock",
"--body", "Final result payload.",
)
var waitResult waitReplyCommandResult
select {
case waitResult = <-waitCh:
case <-time.After(3 * time.Second):
t.Fatal("wait-reply result filter command did not return")
}
if waitResult.exit != 0 {
t.Fatalf("wait-reply result filter failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
mustDecodeJSON(t, waitResult.stdout, &waitResp)
if kind := nestedString(t, waitResp, "data", "message", "kind"); kind != "result" {
t.Fatalf("expected result wake message, got %q", kind)
}
if got := nestedString(t, waitResp, "data", "message", "summary"); got != "Worker completed after unblock" {
t.Fatalf("expected result summary, got %q", got)
}
}
// TestWaitReplyKindsFilterSkipsAnswerUntilControl verifies wait-reply ignores non-matching answers until a matching control message arrives.
func TestWaitReplyKindsFilterSkipsAnswerUntilControl(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
threadID, blockedMessageID := seedBlockedThreadForWaitReply(t, dbPath)
waitCh := make(chan waitReplyCommandResult, 1)
go func() {
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--agent", "worker-c",
"--json",
"wait-reply",
"--thread", threadID,
"--after-message", blockedMessageID,
"--kinds", "control",
"--timeout-seconds", "2",
)
waitCh <- waitReplyCommandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-c",
"--thread", threadID,
"--summary", "Regular answer",
"--body", "This should not wake the control-only waiter.",
)
time.Sleep(200 * time.Millisecond)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"reply",
"--from", "leader",
"--to", "worker-c",
"--thread", threadID,
"--kind", "control",
"--summary", "Pause work",
"--body", "Pause work until product confirms the decision.",
)
var waitResult waitReplyCommandResult
select {
case waitResult = <-waitCh:
case <-time.After(3 * time.Second):
t.Fatal("wait-reply control filter command did not return")
}
if waitResult.exit != 0 {
t.Fatalf("wait-reply control filter failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
mustDecodeJSON(t, waitResult.stdout, &waitResp)
if kind := nestedString(t, waitResp, "data", "message", "kind"); kind != "control" {
t.Fatalf("expected control wake message, got %q", kind)
}
if got := nestedString(t, waitResp, "data", "message", "summary"); got != "Pause work" {
t.Fatalf("expected control summary, got %q", got)
}
}
@@ -0,0 +1,233 @@
package orch
import (
"os"
"path/filepath"
"testing"
)
// TestOrchCleanupAllCompletedRemovesMultipleWorktrees verifies cleanup removes every completed worktree when --all-completed is used.
func TestOrchCleanupAllCompletedRemovesMultipleWorktrees(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
repoPath := initGitRepo(t)
runID := "run_blog_cleanup_all_completed_001"
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Validate cleanup across multiple completed worktrees",
)
worktreeOne := seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, runID, "T1", "worker-a")
worktreeTwo := seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, runID, "T2", "worker-b")
cleanupOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"cleanup",
"--run", runID,
"--all-completed",
)
var cleanupResp map[string]any
mustDecodeJSON(t, cleanupOut, &cleanupResp)
cleaned := nestedArray(t, cleanupResp, "data", "cleaned")
if len(cleaned) != 2 {
t.Fatalf("expected two cleaned attempts, got %#v", cleaned)
}
for _, worktreePath := range []string{worktreeOne, worktreeTwo} {
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
t.Fatalf("expected cleaned worktree path %q to be removed, err=%v", worktreePath, err)
}
}
}
// TestOrchCleanupForceRemovesCompletedWorktrees verifies cleanup accepts --force while removing completed worktrees selected by --all-completed.
func TestOrchCleanupForceRemovesCompletedWorktrees(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
repoPath := initGitRepo(t)
runID := "run_blog_cleanup_force_001"
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Validate cleanup force flag acceptance",
)
worktreePath := seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, runID, "T1", "worker-a")
cleanupOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"cleanup",
"--run", runID,
"--all-completed",
"--force",
)
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 with --force, got %#v", cleaned)
}
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
t.Fatalf("expected forced cleanup worktree path %q to be removed, err=%v", worktreePath, err)
}
}
// TestOrchCouncilStartPersistsCustomInputs verifies council start stores explicit target-file, repo-path, task-id, review mode, and only-unanimous settings.
func TestOrchCouncilStartPersistsCustomInputs(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
repoPath := initGitRepo(t)
targetFile := filepath.Join(tempDir, "target.md")
if err := os.WriteFile(targetFile, []byte("# Review target\n\nInspect the API changes.\n"), 0o644); err != nil {
t.Fatalf("write council target file: %v", err)
}
startOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"council", "start",
"--run", "council_blog_custom_inputs_001",
"--target-file", targetFile,
"--repo-path", repoPath,
"--task-id", "TASK-42",
"--target-type", "repo",
"--mode", "review",
"--output", "json",
"--only-unanimous",
)
var startResp map[string]any
mustDecodeJSON(t, startOut, &startResp)
if got := nestedString(t, startResp, "data", "run_id"); got != "council_blog_custom_inputs_001" {
t.Fatalf("expected council run id, got %q", got)
}
if got := nestedString(t, startResp, "data", "mode"); got != "review" {
t.Fatalf("expected council mode review, got %q", got)
}
reviewers := nestedArray(t, startResp, "data", "reviewers")
if len(reviewers) != 3 {
t.Fatalf("expected three reviewers, got %#v", reviewers)
}
sqlDB, err := openOrchDB(t.Context(), dbPath)
if err != nil {
t.Fatalf("open orch db: %v", err)
}
defer sqlDB.Close()
var (
mode string
targetType string
outputMode string
onlyUnanimous int
)
if err := sqlDB.QueryRowContext(
t.Context(),
`SELECT mode, target_type, output_mode, only_unanimous
FROM council_runs
WHERE run_id = ?`,
"council_blog_custom_inputs_001",
).Scan(&mode, &targetType, &outputMode, &onlyUnanimous); err != nil {
t.Fatalf("query council_runs: %v", err)
}
if mode != "review" || targetType != "repo" || outputMode != "json" || onlyUnanimous != 1 {
t.Fatalf("unexpected council run metadata: mode=%q targetType=%q outputMode=%q onlyUnanimous=%d", mode, targetType, outputMode, onlyUnanimous)
}
var (
storedTargetFile string
storedRepoPath string
storedTaskID string
)
if err := sqlDB.QueryRowContext(
t.Context(),
`SELECT target_file, repo_path, target_task_id
FROM council_inputs
WHERE run_id = ?`,
"council_blog_custom_inputs_001",
).Scan(&storedTargetFile, &storedRepoPath, &storedTaskID); err != nil {
t.Fatalf("query council_inputs: %v", err)
}
if storedTargetFile != targetFile || storedRepoPath != repoPath || storedTaskID != "TASK-42" {
t.Fatalf("unexpected council inputs: targetFile=%q repoPath=%q taskID=%q", storedTargetFile, storedRepoPath, storedTaskID)
}
}
func seedCompletedCodeTaskForCleanupCouncilTests(t *testing.T, dbPath, repoPath, runID, taskID, agent string) string {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", taskID,
"--title", "Complete cleanup target "+taskID,
"--default-to", agent,
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", taskID,
"--execution-mode", "code",
"--repo-path", repoPath,
"--workspace-root", ".orch/worktrees",
)
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")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", agent,
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", agent,
"--thread", threadID,
"--summary", "Completed cleanup target "+taskID,
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", runID,
)
return worktreePath
}
@@ -0,0 +1,330 @@
package orch
import (
"os"
"path/filepath"
"testing"
)
// TestOrchTaskAddRejectsInvalidMetadataJSON verifies orch task add rejects invalid metadata JSON.
func TestOrchTaskAddRejectsInvalidMetadataJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_008",
"--goal", "Validate task metadata input",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_008",
"--task", "T1",
"--title", "Implement retry policy",
"--metadata-json", `{"repo":`,
)
if exitCode != 30 {
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestOrchDepAddCreatesDependencyAndAffectsReadyState verifies orch dep add returns the dependency and blocks the dependent task from ready output.
func TestOrchDepAddCreatesDependencyAndAffectsReadyState(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_dep_001",
"--goal", "Validate dependency contracts",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_dep_001",
"--task", "T1",
"--title", "Prerequisite task",
"--default-to", "worker-a",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_dep_001",
"--task", "T2",
"--title", "Dependent task",
"--default-to", "worker-b",
)
depOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dep", "add",
"--run", "run_blog_dep_001",
"--task", "T2",
"--depends-on", "T1",
)
var depResp map[string]any
mustDecodeJSON(t, depOut, &depResp)
if got := nestedString(t, depResp, "data", "dependency", "run_id"); got != "run_blog_dep_001" {
t.Fatalf("expected dependency run_id run_blog_dep_001, got %q", got)
}
if got := nestedString(t, depResp, "data", "dependency", "task_id"); got != "T2" {
t.Fatalf("expected dependency task_id T2, got %q", got)
}
if got := nestedString(t, depResp, "data", "dependency", "depends_on_task_id"); got != "T1" {
t.Fatalf("expected dependency depends_on_task_id T1, got %q", got)
}
readyOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"ready",
"--run", "run_blog_dep_001",
)
var readyResp map[string]any
mustDecodeJSON(t, readyOut, &readyResp)
readyTasks := nestedArray(t, readyResp, "data", "tasks")
if len(readyTasks) != 1 {
t.Fatalf("expected one ready task after dependency add, got %#v", readyTasks)
}
readyTask, ok := readyTasks[0].(map[string]any)
if !ok {
t.Fatalf("expected ready task object, got %#v", readyTasks[0])
}
if got, _ := readyTask["task_id"].(string); got != "T1" {
t.Fatalf("expected only prerequisite task T1 to remain ready, got %#v", readyTask["task_id"])
}
}
// TestOrchDepAddRejectsMissingDependencyTask verifies orch dep add rejects a dependency on a missing task.
func TestOrchDepAddRejectsMissingDependencyTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_dep_002",
"--goal", "Validate dependency not found",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_dep_002",
"--task", "T2",
"--title", "Dependent task",
"--default-to", "worker-b",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"dep", "add",
"--run", "run_blog_dep_002",
"--task", "T2",
"--depends-on", "T9",
)
if exitCode != 40 {
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "not_found")
}
// TestOrchDepAddRejectsMissingTask verifies orch dep add rejects a missing source task.
func TestOrchDepAddRejectsMissingTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_dep_003",
"--goal", "Validate source task not found",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_dep_003",
"--task", "T1",
"--title", "Prerequisite task",
"--default-to", "worker-a",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"dep", "add",
"--run", "run_blog_dep_003",
"--task", "T9",
"--depends-on", "T1",
)
if exitCode != 40 {
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "not_found")
}
// TestOrchVerifyStatusReturnsTaskAttemptSpecAndGate verifies orch verify status returns task, attempt, spec, and gate details.
func TestOrchVerifyStatusReturnsTaskAttemptSpecAndGate(t *testing.T) {
t.Parallel()
dbPath, specFile := seedVerifyStatusTaskForCoreContracts(t, "run_verify_core_001", true)
verifyStatusOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "status",
"--run", "run_verify_core_001",
"--task", "T1",
)
var verifyStatusResp map[string]any
mustDecodeJSON(t, verifyStatusOut, &verifyStatusResp)
if got := nestedString(t, verifyStatusResp, "data", "task", "task_id"); got != "T1" {
t.Fatalf("expected task_id T1, got %q", got)
}
if got := nestedString(t, verifyStatusResp, "data", "task", "status"); got != "verifying" {
t.Fatalf("expected task status verifying, got %q", got)
}
if got := nestedString(t, verifyStatusResp, "data", "attempt", "assigned_to"); got != "worker-a" {
t.Fatalf("expected attempt assigned_to worker-a, got %q", got)
}
if got := nestedString(t, verifyStatusResp, "data", "spec", "spec_file"); got != specFile {
t.Fatalf("expected spec_file %q, got %q", specFile, got)
}
requiredChecks := nestedArray(t, verifyStatusResp, "data", "gate", "required_checks")
if len(requiredChecks) != 1 || requiredChecks[0] != "lint" {
t.Fatalf("expected gate required_checks [lint], got %#v", requiredChecks)
}
pendingChecks := nestedArray(t, verifyStatusResp, "data", "gate", "pending_checks")
if len(pendingChecks) != 1 || pendingChecks[0] != "lint" {
t.Fatalf("expected gate pending_checks [lint], got %#v", pendingChecks)
}
}
// TestOrchVerifyStatusRejectsMissingExplicitAttempt verifies orch verify status rejects an explicit attempt that does not exist.
func TestOrchVerifyStatusRejectsMissingExplicitAttempt(t *testing.T) {
t.Parallel()
dbPath, _ := seedVerifyStatusTaskForCoreContracts(t, "run_verify_core_002", false)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"verify", "status",
"--run", "run_verify_core_002",
"--task", "T1",
"--attempt", "2",
)
if exitCode != 30 {
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_state")
}
func seedVerifyStatusTaskForCoreContracts(t *testing.T, runID string, moveToVerifying bool) (string, string) {
t.Helper()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
specFile := filepath.Join(tempDir, "task.md")
if err := os.WriteFile(specFile, []byte("# Task\n\nShip the verifier-backed component change.\n"), 0o644); err != nil {
t.Fatalf("write spec file: %v", err)
}
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Seed verify status contract task",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", "T1",
"--title", "Implement verifier-backed task",
"--default-to", "worker-a",
"--spec-file", specFile,
"--required-check", "lint",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", "T1",
"--execution-mode", "analysis",
"--body", "Implement the gated task.",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
if moveToVerifying {
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Implementation finished",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", runID,
)
}
return dbPath, specFile
}
@@ -0,0 +1,136 @@
package orch
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestOrchAnswerPropagatesBodyFileContent verifies orch answer reads the leader response body from a file.
func TestOrchAnswerPropagatesBodyFileContent(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
bodyPath := filepath.Join(tempDir, "answer.md")
body := "Use stdout for the MVP logs.\nKeep stderr for unexpected failures.\n"
if err := os.WriteFile(bodyPath, []byte(body), 0o644); err != nil {
t.Fatalf("write answer body file: %v", err)
}
threadID := seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_file_001", "T2", "worker-b")
answerOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"answer",
"--run", "run_blog_answer_file_001",
"--task", "T2",
"--body-file", bodyPath,
)
var answerResp map[string]any
mustDecodeJSON(t, answerOut, &answerResp)
if got := nestedString(t, answerResp, "data", "message", "kind"); got != "answer" {
t.Fatalf("expected answer message kind, got %q", got)
}
if got := nestedString(t, answerResp, "data", "message", "body"); got != body {
t.Fatalf("expected answer body %q, got %q", body, got)
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", threadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
messages := nestedArray(t, showResp, "data", "messages")
lastMessage, ok := messages[len(messages)-1].(map[string]any)
if !ok {
t.Fatalf("expected last message object, got %#v", messages[len(messages)-1])
}
if got, _ := lastMessage["body"].(string); got != body {
t.Fatalf("expected latest inbox message body %q, got %#v", body, lastMessage["body"])
}
}
// TestOrchAnswerRejectsWithoutActiveBlockedQuestion verifies orch answer rejects tasks without an active blocked question.
func TestOrchAnswerRejectsWithoutActiveBlockedQuestion(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
seedDispatchedTaskForAnswerEdgeTests(t, dbPath, "run_blog_answer_ready_001", "T2", "worker-b")
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"answer",
"--run", "run_blog_answer_ready_001",
"--task", "T2",
"--body", "Use stdout for the MVP logs.",
)
if exitCode != 30 {
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_state")
assertErrorMessageContains(t, stdout, "blocked")
}
// TestOrchBlockedHelpMentionsCompactQueue verifies blocked help explains the compact queue leaders inspect before answering.
func TestOrchBlockedHelpMentionsCompactQueue(t *testing.T) {
t.Parallel()
stdout, stderr, exitCode := executeOrchCommand("blocked", "--help")
if exitCode != 0 {
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
}
combined := stdout + stderr
if !strings.Contains(combined, "latest question the leader needs to answer") {
t.Fatalf("expected blocked help to explain latest question role, got:\n%s", combined)
}
if !strings.Contains(combined, "Use blocked before answer") {
t.Fatalf("expected blocked help to explain inspect-before-answer flow, got:\n%s", combined)
}
if !strings.Contains(combined, "compact queue of unresolved worker questions") {
t.Fatalf("expected blocked help to explain compact queue role, got:\n%s", combined)
}
}
func seedDispatchedTaskForAnswerEdgeTests(t *testing.T, dbPath, runID, taskID, agent string) {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Prepare dispatched task for answer edge tests",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", taskID,
"--title", "Build frontend",
"--default-to", agent,
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", taskID,
"--execution-mode", "analysis",
)
}
@@ -0,0 +1,547 @@
package orch
import (
"os"
"path/filepath"
"testing"
)
// TestOrchDispatchReadsBodyFromBodyFileAndCarriesSpecPayload verifies dispatch loads body-file input and includes spec and verification policy in the task payload.
func TestOrchDispatchReadsBodyFromBodyFileAndCarriesSpecPayload(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
specFile := filepath.Join(tempDir, "task.md")
bodyFile := filepath.Join(tempDir, "dispatch-body.txt")
specBody := "# Task\n\nImplement the spec-aware worker handoff.\n"
dispatchBody := "Worker brief loaded from file.\n"
if err := os.WriteFile(specFile, []byte(specBody), 0o644); err != nil {
t.Fatalf("write spec file: %v", err)
}
if err := os.WriteFile(bodyFile, []byte(dispatchBody), 0o644); err != nil {
t.Fatalf("write body file: %v", err)
}
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_dispatch_contracts_001",
"--goal", "Validate dispatch payload contracts",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_dispatch_contracts_001",
"--task", "T1",
"--title", "Implement worker handoff",
"--summary", "Ship the spec-aware task payload",
"--default-to", "worker-a",
"--spec-file", specFile,
"--check-profile", "cadence_component",
"--required-check", "lint",
"--allowed-path", "packages/orch-runtime",
"--blocked-path", "scripts/release.sh",
"--metadata-json", `{"team":"core"}`,
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_dispatch_contracts_001",
"--task", "T1",
"--execution-mode", "analysis",
"--to", "worker-a",
"--body-file", bodyFile,
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
if got := nestedString(t, dispatchResp, "data", "message", "body"); got != dispatchBody {
t.Fatalf("expected dispatch body %q, got %q", dispatchBody, got)
}
if got := nestedString(t, dispatchResp, "data", "task", "status"); got != "dispatched" {
t.Fatalf("expected task status dispatched, got %q", got)
}
payload, ok := nestedValue(t, dispatchResp, "data", "message", "payload_json").(map[string]any)
if !ok {
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, dispatchResp, "data", "message", "payload_json"))
}
if got, _ := payload["execution_mode"].(string); got != "analysis" {
t.Fatalf("expected execution_mode analysis, got %#v", payload["execution_mode"])
}
spec, ok := payload["spec"].(map[string]any)
if !ok {
t.Fatalf("expected payload spec object, got %#v", payload["spec"])
}
if got, _ := spec["file"].(string); got != specFile {
t.Fatalf("expected spec file %q, got %#v", specFile, spec["file"])
}
if got, _ := spec["body"].(string); got != specBody {
t.Fatalf("expected spec body %q, got %#v", specBody, spec["body"])
}
if got, _ := spec["check_profile"].(string); got != "cadence_component" {
t.Fatalf("expected check_profile cadence_component, got %#v", spec["check_profile"])
}
requiredChecks, ok := spec["required_checks"].([]any)
if !ok || len(requiredChecks) != 1 || requiredChecks[0] != "lint" {
t.Fatalf("expected required_checks [lint], got %#v", spec["required_checks"])
}
allowedPaths, ok := spec["allowed_paths"].([]any)
if !ok || len(allowedPaths) != 1 || allowedPaths[0] != "packages/orch-runtime" {
t.Fatalf("expected allowed_paths [packages/orch-runtime], got %#v", spec["allowed_paths"])
}
blockedPaths, ok := spec["blocked_paths"].([]any)
if !ok || len(blockedPaths) != 1 || blockedPaths[0] != "scripts/release.sh" {
t.Fatalf("expected blocked_paths [scripts/release.sh], got %#v", spec["blocked_paths"])
}
metadata, ok := spec["metadata"].(map[string]any)
if !ok {
t.Fatalf("expected payload spec metadata object, got %#v", spec["metadata"])
}
if got, _ := metadata["team"].(string); got != "core" {
t.Fatalf("expected payload spec metadata.team core, got %#v", metadata["team"])
}
}
// TestOrchVerifyRecordMovesTaskToFailedOnFailedCheck verifies verify record moves a verifying task to failed when a required check fails.
func TestOrchVerifyRecordMovesTaskToFailedOnFailedCheck(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_contracts_001", "T1", []string{"lint"})
verifyOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "record",
"--run", "run_verify_contracts_001",
"--task", "T1",
"--check", "lint",
"--status", "failed",
"--summary", "lint failed",
)
var verifyResp map[string]any
mustDecodeJSON(t, verifyOut, &verifyResp)
if got := nestedString(t, verifyResp, "data", "task", "status"); got != "failed" {
t.Fatalf("expected task status failed, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "gate", "status"); got != "failed" {
t.Fatalf("expected gate status failed, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "check", "status"); got != "failed" {
t.Fatalf("expected recorded check status failed, got %q", got)
}
}
// TestOrchVerifyRecordUpdatesExistingNamedCheck verifies verify record updates an existing named check result and recomputes the gate.
func TestOrchVerifyRecordUpdatesExistingNamedCheck(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_contracts_002", "T1", []string{"lint", "test"})
runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "record",
"--run", "run_verify_contracts_002",
"--task", "T1",
"--check", "lint",
"--status", "failed",
"--summary", "first lint failure",
"--recorded-by", "qa-1",
)
verifyOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "record",
"--run", "run_verify_contracts_002",
"--task", "T1",
"--check", "lint",
"--status", "passed",
"--summary", "lint fixed",
"--recorded-by", "qa-2",
)
var verifyResp map[string]any
mustDecodeJSON(t, verifyOut, &verifyResp)
if got := nestedString(t, verifyResp, "data", "task", "status"); got != "verifying" {
t.Fatalf("expected task status verifying after updating lint result, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "gate", "status"); got != "pending" {
t.Fatalf("expected gate status pending after updating lint result, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "check", "status"); got != "passed" {
t.Fatalf("expected updated check status passed, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "check", "summary"); got != "lint fixed" {
t.Fatalf("expected updated check summary lint fixed, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "check", "recorded_by"); got != "qa-2" {
t.Fatalf("expected updated check recorded_by qa-2, got %q", got)
}
verifyStatusOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "status",
"--run", "run_verify_contracts_002",
"--task", "T1",
)
var verifyStatusResp map[string]any
mustDecodeJSON(t, verifyStatusOut, &verifyStatusResp)
pendingChecks := nestedArray(t, verifyStatusResp, "data", "gate", "pending_checks")
if len(pendingChecks) != 1 || pendingChecks[0] != "test" {
t.Fatalf("expected only test to remain pending after lint update, got %#v", pendingChecks)
}
}
// TestOrchVerifyRecordRoundTripsBodyMetadataAndRecorder verifies verify record preserves body-file content, metadata JSON, and recorder identity in the returned check result.
func TestOrchVerifyRecordRoundTripsBodyMetadataAndRecorder(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
bodyFile := filepath.Join(tempDir, "verify.txt")
body := "verification details from file\n"
if err := os.WriteFile(bodyFile, []byte(body), 0o644); err != nil {
t.Fatalf("write verify body file: %v", err)
}
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_contracts_003", "T1", []string{"lint"})
verifyOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "record",
"--run", "run_verify_contracts_003",
"--task", "T1",
"--check", "lint",
"--status", "passed",
"--summary", "lint clean",
"--body-file", bodyFile,
"--metadata-json", `{"ticket":"QA-1"}`,
"--recorded-by", "qa-bot",
)
var verifyResp map[string]any
mustDecodeJSON(t, verifyOut, &verifyResp)
if got := nestedString(t, verifyResp, "data", "task", "status"); got != "done" {
t.Fatalf("expected task status done after the only required check passes, got %q", got)
}
if got := nestedString(t, verifyResp, "data", "check", "body"); got != body {
t.Fatalf("expected check body %q, got %q", body, got)
}
if got := nestedString(t, verifyResp, "data", "check", "recorded_by"); got != "qa-bot" {
t.Fatalf("expected check recorded_by qa-bot, got %q", got)
}
metadata, ok := nestedValue(t, verifyResp, "data", "check", "metadata_json").(map[string]any)
if !ok {
t.Fatalf("expected check metadata_json object, got %#v", nestedValue(t, verifyResp, "data", "check", "metadata_json"))
}
if got, _ := metadata["ticket"].(string); got != "QA-1" {
t.Fatalf("expected metadata_json.ticket QA-1, got %#v", metadata["ticket"])
}
}
// TestOrchRetryRejectsNonFailedTask verifies retry rejects tasks that are not currently failed.
func TestOrchRetryRejectsNonFailedTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_retry_contracts_001",
"--goal", "Validate retry state guards",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_retry_contracts_001",
"--task", "T1",
"--title", "Implement retry guard",
"--default-to", "worker-a",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_retry_contracts_001",
"--task", "T1",
"--execution-mode", "analysis",
"--to", "worker-a",
)
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"retry",
"--run", "run_retry_contracts_001",
"--task", "T1",
)
if exitCode != 30 {
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_state")
}
// TestOrchRetryUsesBodyFileForReplacementAttempt verifies retry loads the replacement worker brief from body-file.
func TestOrchRetryUsesBodyFileForReplacementAttempt(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "coord.db")
bodyFile := filepath.Join(tempDir, "retry-body.txt")
body := "Retry after the failure with the updated brief.\n"
if err := os.WriteFile(bodyFile, []byte(body), 0o644); err != nil {
t.Fatalf("write retry body file: %v", err)
}
originalThreadID := seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_retry_contracts_002", "T1")
retryOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"retry",
"--run", "run_retry_contracts_002",
"--task", "T1",
"--body-file", bodyFile,
)
var retryResp map[string]any
mustDecodeJSON(t, retryOut, &retryResp)
if got := nestedString(t, retryResp, "data", "task", "status"); got != "dispatched" {
t.Fatalf("expected task status dispatched after retry, got %q", got)
}
newThreadID := nestedString(t, retryResp, "data", "attempt", "thread_id")
if newThreadID == originalThreadID {
t.Fatalf("expected retry to create a new thread, got %q", newThreadID)
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", newThreadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
lastMessage := lastThreadMessageForVerifyAndRecoveryTests(t, showResp)
if got, _ := lastMessage["body"].(string); got != body {
t.Fatalf("expected retry message body %q, got %#v", body, lastMessage["body"])
}
}
// TestOrchReassignWorksFromFailedTask verifies reassign can move a failed task to a new worker by creating a replacement attempt.
func TestOrchReassignWorksFromFailedTask(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
originalThreadID := seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_reassign_contracts_001", "T1")
reassignOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"reassign",
"--run", "run_reassign_contracts_001",
"--task", "T1",
"--to", "worker-b",
"--reason", "Route the failed task to a different worker.",
)
var reassignResp map[string]any
mustDecodeJSON(t, reassignOut, &reassignResp)
if got := nestedString(t, reassignResp, "data", "task", "status"); got != "dispatched" {
t.Fatalf("expected task status dispatched after reassign, got %q", got)
}
if got := nestedString(t, reassignResp, "data", "attempt", "assigned_to"); got != "worker-b" {
t.Fatalf("expected reassigned attempt to target worker-b, got %q", got)
}
if got := nestedValue(t, reassignResp, "data", "attempt", "attempt_no").(float64); got != 2 {
t.Fatalf("expected reassign attempt 2, got %#v", got)
}
newThreadID := nestedString(t, reassignResp, "data", "attempt", "thread_id")
if newThreadID == originalThreadID {
t.Fatalf("expected reassign to create a new thread, got %q", newThreadID)
}
}
func seedVerifyingTaskForVerifyAndRecoveryTests(t *testing.T, dbPath, runID, taskID string, requiredChecks []string) string {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Prepare verifying task for contract tests",
)
args := []string{
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", taskID,
"--title", "Prepare verifying task",
"--default-to", "worker-a",
}
for _, check := range requiredChecks {
args = append(args, "--required-check", check)
}
runOrchCommand(t, args...)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", taskID,
"--execution-mode", "analysis",
"--to", "worker-a",
"--body", "Prepare the task for verification.",
)
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", "Implementation complete",
"--body", "Ready for verification.",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", runID,
)
return threadID
}
func seedFailedTaskForVerifyAndRecoveryTests(t *testing.T, dbPath, runID, taskID string) string {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Prepare failed task for recovery tests",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", taskID,
"--title", "Prepare failed task",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", taskID,
"--execution-mode", "analysis",
"--to", "worker-a",
"--body", "Initial worker brief.",
)
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", "Worker reported a failure",
"--body", "The task failed before completion.",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", runID,
)
return threadID
}
func lastThreadMessageForVerifyAndRecoveryTests(t *testing.T, showResp map[string]any) map[string]any {
t.Helper()
messages := nestedArray(t, showResp, "data", "messages")
if len(messages) == 0 {
t.Fatalf("expected at least one message, got %#v", messages)
}
lastMessage, ok := messages[len(messages)-1].(map[string]any)
if !ok {
t.Fatalf("expected message object, got %#v", messages[len(messages)-1])
}
return lastMessage
}
@@ -0,0 +1,143 @@
package orch
import (
"path/filepath"
"testing"
)
// TestOrchAnswerRejectsInvalidPayloadJSON verifies answer rejects malformed payload JSON.
func TestOrchAnswerRejectsInvalidPayloadJSON(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
_ = seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_extra_002", "T2", "worker-b")
stdout, _, exitCode := executeOrchCommand(
"--db", dbPath,
"--json",
"answer",
"--run", "run_blog_answer_extra_002",
"--task", "T2",
"--payload-json", "not-json",
)
if exitCode != 30 {
t.Fatalf("expected exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
}
assertErrorJSON(t, stdout, "invalid_input")
}
// TestOrchVerifyStatusSelectsExplicitAttempt verifies verify status can inspect a non-latest attempt by number.
func TestOrchVerifyStatusSelectsExplicitAttempt(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runID := "run_blog_verify_extra_002"
seedVerifyingTaskForVerifyContractTests(t, dbPath, runID, "T1")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "record",
"--run", runID,
"--task", "T1",
"--check", "lint",
"--status", "failed",
"--summary", "Lint failed",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"retry",
"--run", runID,
"--task", "T1",
"--body", "Retry after fixing lint.",
)
verifyStatusOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"verify", "status",
"--run", runID,
"--task", "T1",
"--attempt", "1",
)
var verifyStatusResp map[string]any
mustDecodeJSON(t, verifyStatusOut, &verifyStatusResp)
if got := nestedValue(t, verifyStatusResp, "data", "attempt", "attempt_no").(float64); got != 1 {
t.Fatalf("expected verify status to select attempt 1, got %#v", got)
}
if got := nestedString(t, verifyStatusResp, "data", "attempt", "task_id"); got != "T1" {
t.Fatalf("expected verify status attempt task T1, got %q", got)
}
if got := nestedValue(t, verifyStatusResp, "data", "gate", "attempt_no").(float64); got != 1 {
t.Fatalf("expected verify gate attempt 1, got %#v", got)
}
}
func seedVerifyingTaskForVerifyContractTests(t *testing.T, dbPath, runID, taskID string) {
t.Helper()
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", runID,
"--goal", "Prepare verify contract task",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", runID,
"--task", taskID,
"--title", "Implement gated task",
"--default-to", "worker-a",
"--required-check", "lint",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", runID,
"--task", taskID,
"--execution-mode", "analysis",
"--body", "Implement the gated task.",
)
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", "Task complete",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", runID,
)
}