Add orch and inbox CLI gap-fill tests
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchRunInitRejectsDuplicateRun verifies orch run init rejects a duplicate run ID.
|
||||
func TestOrchRunInitRejectsDuplicateRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_dup_001",
|
||||
"--goal", "Create the initial run",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_dup_001",
|
||||
"--goal", "Try creating the same run again",
|
||||
)
|
||||
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, "already exists")
|
||||
}
|
||||
|
||||
// TestOrchRunShowPlainTextOutput verifies orch run show prints the human-readable summary.
|
||||
func TestOrchRunShowPlainTextOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_show_plain_001",
|
||||
"--goal", "Inspect plain-text status output",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_show_plain_001",
|
||||
"--task", "T1",
|
||||
"--title", "Make the run ready",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"run", "show",
|
||||
"--run", "run_blog_show_plain_001",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success exit code, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if stderr != "" {
|
||||
t.Fatalf("expected empty stderr, got:\n%s", stderr)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "run run_blog_show_plain_001 status ready" {
|
||||
t.Fatalf("expected plain-text run summary, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchTaskAddRejectsDuplicateTask verifies orch task add rejects a duplicate task ID in one run.
|
||||
func TestOrchTaskAddRejectsDuplicateTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_task_dup_001", "Validate duplicate task rejection")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_task_dup_001",
|
||||
"--task", "T1",
|
||||
"--title", "Create the original task",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_task_dup_001",
|
||||
"--task", "T1",
|
||||
"--title", "Create the duplicate task",
|
||||
)
|
||||
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, "already exists")
|
||||
}
|
||||
|
||||
// TestOrchTaskAddRejectsSpecSHAWithoutSpecFile verifies orch task add rejects spec-sha when spec-file is missing.
|
||||
func TestOrchTaskAddRejectsSpecSHAWithoutSpecFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_task_sha_001", "Validate spec-sha guard")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_task_sha_001",
|
||||
"--task", "T1",
|
||||
"--title", "Reject missing spec file",
|
||||
"--spec-sha", "deadbeef",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "spec-sha requires spec-file")
|
||||
}
|
||||
|
||||
// TestOrchTaskAddRejectsUnreadableSpecFile verifies orch task add rejects an unreadable spec file.
|
||||
func TestOrchTaskAddRejectsUnreadableSpecFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_task_spec_001", "Validate unreadable spec file")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_task_spec_001",
|
||||
"--task", "T1",
|
||||
"--title", "Reject unreadable spec file",
|
||||
"--spec-file", filepath.Join(tempDir, "missing-task.md"),
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "failed to read spec-file")
|
||||
}
|
||||
|
||||
// TestOrchDepAddRejectsSelfDependency verifies orch dep add rejects a task depending on itself.
|
||||
func TestOrchDepAddRejectsSelfDependency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_dep_self_001", "Validate self-dependency")
|
||||
seedCoreAdditionalTask(t, dbPath, "run_blog_dep_self_001", "T1", "One task")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dep", "add",
|
||||
"--run", "run_blog_dep_self_001",
|
||||
"--task", "T1",
|
||||
"--depends-on", "T1",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "task cannot depend on itself")
|
||||
}
|
||||
|
||||
// TestOrchDepAddRejectsDuplicateDependency verifies orch dep add rejects a duplicate edge.
|
||||
func TestOrchDepAddRejectsDuplicateDependency(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_dep_dup_001", "Validate duplicate dependency")
|
||||
seedCoreAdditionalTask(t, dbPath, "run_blog_dep_dup_001", "T1", "Prerequisite task")
|
||||
seedCoreAdditionalTask(t, dbPath, "run_blog_dep_dup_001", "T2", "Dependent task")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dep", "add",
|
||||
"--run", "run_blog_dep_dup_001",
|
||||
"--task", "T2",
|
||||
"--depends-on", "T1",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dep", "add",
|
||||
"--run", "run_blog_dep_dup_001",
|
||||
"--task", "T2",
|
||||
"--depends-on", "T1",
|
||||
)
|
||||
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, "already exists")
|
||||
}
|
||||
|
||||
// TestOrchReadyRejectsMissingRun verifies orch ready rejects a missing run.
|
||||
func TestOrchReadyRejectsMissingRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"ready",
|
||||
"--run", "run_blog_ready_missing_001",
|
||||
)
|
||||
if exitCode != 40 {
|
||||
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "not_found")
|
||||
}
|
||||
|
||||
// TestOrchReadyPrintsNoReadyTasks verifies orch ready prints the no-work message for an empty run.
|
||||
func TestOrchReadyPrintsNoReadyTasks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_ready_empty_001", "Validate empty ready queue")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"ready",
|
||||
"--run", "run_blog_ready_empty_001",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success exit code, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if stderr != "" {
|
||||
t.Fatalf("expected empty stderr, got:\n%s", stderr)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "no ready tasks" {
|
||||
t.Fatalf("expected no ready tasks message, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchReadyPrintsPlainTextTasks verifies orch ready prints tabular plain-text results.
|
||||
func TestOrchReadyPrintsPlainTextTasks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_ready_plain_001", "Validate plain-text ready output")
|
||||
seedCoreAdditionalTask(t, dbPath, "run_blog_ready_plain_001", "T1", "First task")
|
||||
seedCoreAdditionalTask(t, dbPath, "run_blog_ready_plain_001", "T2", "Second task")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"ready",
|
||||
"--run", "run_blog_ready_plain_001",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success exit code, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if stderr != "" {
|
||||
t.Fatalf("expected empty stderr, got:\n%s", stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "T1\tnormal\tFirst task") {
|
||||
t.Fatalf("expected plain-text output to contain T1 row, got:\n%s", stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "T2\tnormal\tSecond task") {
|
||||
t.Fatalf("expected plain-text output to contain T2 row, got:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchReadyDefaultsLimitWhenNonPositive verifies orch ready falls back to the default limit when limit is non-positive.
|
||||
func TestOrchReadyDefaultsLimitWhenNonPositive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedCoreAdditionalRun(t, dbPath, "run_blog_ready_limit_001", "Validate default ready limit")
|
||||
for i := 1; i <= 25; i++ {
|
||||
seedCoreAdditionalTask(
|
||||
t,
|
||||
dbPath,
|
||||
"run_blog_ready_limit_001",
|
||||
fmt.Sprintf("T%02d", i),
|
||||
fmt.Sprintf("Task %02d", i),
|
||||
)
|
||||
}
|
||||
|
||||
readyOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"ready",
|
||||
"--run", "run_blog_ready_limit_001",
|
||||
"--limit", "0",
|
||||
)
|
||||
|
||||
var readyResp map[string]any
|
||||
mustDecodeJSON(t, readyOut, &readyResp)
|
||||
readyTasks := nestedArray(t, readyResp, "data", "tasks")
|
||||
if len(readyTasks) != 20 {
|
||||
t.Fatalf("expected default ready limit of 20 tasks, got %#v", len(readyTasks))
|
||||
}
|
||||
}
|
||||
|
||||
func seedCoreAdditionalRun(t *testing.T, dbPath, runID, goal string) {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", goal,
|
||||
)
|
||||
}
|
||||
|
||||
func seedCoreAdditionalTask(t *testing.T, dbPath, runID, taskID, title string) {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", title,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchDispatchRejectsInvalidExecutionMode verifies dispatch rejects unknown execution modes.
|
||||
func TestOrchDispatchRejectsInvalidExecutionMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_dispatch_invalid_mode_001",
|
||||
"--goal", "Validate dispatch mode guards",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_dispatch_invalid_mode_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_dispatch_invalid_mode_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "review",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "execution-mode must be one of: analysis, code")
|
||||
}
|
||||
|
||||
// TestOrchDispatchRejectsCodeOnlyFlagsInAnalysisMode verifies analysis mode rejects code-only flags.
|
||||
func TestOrchDispatchRejectsCodeOnlyFlagsInAnalysisMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
message string
|
||||
}{
|
||||
{
|
||||
name: "repo-path",
|
||||
args: []string{"--repo-path", "/tmp/repo"},
|
||||
message: "repo-path is only valid with --execution-mode code",
|
||||
},
|
||||
{
|
||||
name: "workspace-root",
|
||||
args: []string{"--workspace-root", ".orch/worktrees"},
|
||||
message: "workspace-root is only valid with --execution-mode code",
|
||||
},
|
||||
{
|
||||
name: "base-ref",
|
||||
args: []string{"--base-ref", "HEAD"},
|
||||
message: "base-ref is only valid with --execution-mode code",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
args := append([]string{
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_unused",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
}, tc.args...)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(args...)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, tc.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchDispatchRejectsMissingTargetAgent verifies dispatch requires either --to or task default_to.
|
||||
func TestOrchDispatchRejectsMissingTargetAgent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_dispatch_target_001",
|
||||
"--goal", "Validate dispatch target guards",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_dispatch_target_001",
|
||||
"--task", "T1",
|
||||
"--title", "Investigate logs",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_dispatch_target_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "dispatch target agent is required")
|
||||
}
|
||||
|
||||
// TestOrchDispatchCodeModeUsesCurrentWorkingDirectoryAndDefaultWorkspaceRoot verifies code-mode dispatch can infer repo and workspace root.
|
||||
func TestOrchDispatchCodeModeUsesCurrentWorkingDirectoryAndDefaultWorkspaceRoot(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
repoPath := initGitRepo(t)
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_dispatch_cwd_001",
|
||||
"--goal", "Validate cwd repo inference",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_dispatch_cwd_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommandInDir(
|
||||
repoPath,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_dispatch_cwd_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "code",
|
||||
"--body", "Work from the current repository.",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected successful dispatch from cwd, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, stdout, &dispatchResp)
|
||||
worktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
|
||||
expectedPath := buildAttemptWorktreePath(filepath.Join(repoPath, ".orch", "worktrees"), "run_dispatch_cwd_001", "T1", 1)
|
||||
if worktreePath != expectedPath {
|
||||
t.Fatalf("expected inferred worktree path %q, got %q", expectedPath, worktreePath)
|
||||
}
|
||||
if _, err := os.Stat(worktreePath); err != nil {
|
||||
t.Fatalf("stat inferred worktree path %s: %v", worktreePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchDispatchCodeModeRejectsNonGitRepoPath verifies code-mode dispatch rejects non-Git paths.
|
||||
func TestOrchDispatchCodeModeRejectsNonGitRepoPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
nonGitPath := filepath.Join(t.TempDir(), "not-a-repo")
|
||||
if err := os.MkdirAll(nonGitPath, 0o755); err != nil {
|
||||
t.Fatalf("mkdir non-git path: %v", err)
|
||||
}
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_dispatch_repo_guard_001",
|
||||
"--goal", "Validate repo path guard",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_dispatch_repo_guard_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_dispatch_repo_guard_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "code",
|
||||
"--repo-path", nonGitPath,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "repo-path must point to a Git worktree")
|
||||
}
|
||||
|
||||
// TestOrchDispatchCodeModeRejectsBadBaseRef verifies code-mode dispatch rejects unresolved base refs.
|
||||
func TestOrchDispatchCodeModeRejectsBadBaseRef(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
repoPath := initGitRepo(t)
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_dispatch_base_ref_001",
|
||||
"--goal", "Validate base ref guard",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_dispatch_base_ref_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_dispatch_base_ref_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "code",
|
||||
"--repo-path", repoPath,
|
||||
"--base-ref", "does-not-exist",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "base-ref must resolve to a commit")
|
||||
}
|
||||
|
||||
// TestOrchReconcileMapsCancelledThreadsAndNoOpsWhenRepeated verifies reconcile maps cancelled threads once and then becomes a no-op.
|
||||
func TestOrchReconcileMapsCancelledThreadsAndNoOpsWhenRepeated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_reconcile_cancelled_001",
|
||||
"--goal", "Validate cancelled reconcile mapping",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_reconcile_cancelled_001",
|
||||
"--task", "T1",
|
||||
"--title", "Investigate logs",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_reconcile_cancelled_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cancel",
|
||||
"--agent", "leader",
|
||||
"--thread", threadID,
|
||||
"--reason", "Task cancelled by operator.",
|
||||
)
|
||||
|
||||
reconcileOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", "run_reconcile_cancelled_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 cancelled reconcile, got %#v", updatedTasks)
|
||||
}
|
||||
task := updatedTasks[0].(map[string]any)
|
||||
if got, _ := task["status"].(string); got != "cancelled" {
|
||||
t.Fatalf("expected cancelled task status, got %#v", task["status"])
|
||||
}
|
||||
taskCounts := nestedValue(t, reconcileResp, "data", "task_counts").(map[string]any)
|
||||
if got, _ := taskCounts["cancelled"].(float64); got != 1 {
|
||||
t.Fatalf("expected cancelled task count 1, got %#v", taskCounts["cancelled"])
|
||||
}
|
||||
|
||||
secondReconcileOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", "run_reconcile_cancelled_001",
|
||||
)
|
||||
|
||||
var secondResp map[string]any
|
||||
mustDecodeJSON(t, secondReconcileOut, &secondResp)
|
||||
if updated := nestedArray(t, secondResp, "data", "updated_tasks"); len(updated) != 0 {
|
||||
t.Fatalf("expected repeated reconcile to be a no-op, got %#v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchStatusRejectsMissingRun verifies status reports not_found for unknown runs.
|
||||
func TestOrchStatusRejectsMissingRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"status",
|
||||
"--run", "run_status_missing_001",
|
||||
)
|
||||
if exitCode != 40 {
|
||||
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "not_found")
|
||||
}
|
||||
|
||||
// TestOrchStatusPlainTextOutput verifies status renders the human-readable dashboard view.
|
||||
func TestOrchStatusPlainTextOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_status_text_001",
|
||||
"--goal", "Validate status plain-text output",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_status_text_001",
|
||||
"--task", "T1",
|
||||
"--title", "Investigate flaky tests",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"status",
|
||||
"--run", "run_status_text_001",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected status exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "run run_status_text_001 status ready") {
|
||||
t.Fatalf("expected run summary in plain-text status output, got:\n%s", stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "T1\tready\tInvestigate flaky tests") {
|
||||
t.Fatalf("expected task row in plain-text status output, got:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchWaitNormalizesForFlagAndReturnsExistingEventsImmediately verifies wait normalizes event filters and returns existing events.
|
||||
func TestOrchWaitNormalizesForFlagAndReturnsExistingEventsImmediately(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_wait_existing_001",
|
||||
"--goal", "Validate wait filter normalization",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_wait_existing_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_wait_existing_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
runInboxCommandEventually(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"update",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
"--status", "blocked",
|
||||
"--summary", "Need API decision",
|
||||
"--payload-json", `{"question":"Use v1 or v2?"}`,
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", "run_wait_existing_001",
|
||||
)
|
||||
|
||||
waitOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"wait",
|
||||
"--run", "run_wait_existing_001",
|
||||
"--for", " task_blocked , task_blocked , ",
|
||||
"--after-event", "0",
|
||||
)
|
||||
|
||||
var waitResp map[string]any
|
||||
mustDecodeJSON(t, waitOut, &waitResp)
|
||||
if woke, _ := nestedValue(t, waitResp, "data", "woke").(bool); !woke {
|
||||
t.Fatalf("expected wait to wake on an existing blocked event, got %#v", waitResp)
|
||||
}
|
||||
events := nestedArray(t, waitResp, "data", "events")
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected one matching blocked event, got %#v", events)
|
||||
}
|
||||
event := events[0].(map[string]any)
|
||||
if got, _ := event["type"].(string); got != "task_blocked" {
|
||||
t.Fatalf("expected task_blocked event, got %#v", event["type"])
|
||||
}
|
||||
if got, _ := event["summary"].(string); got != "Need API decision" {
|
||||
t.Fatalf("expected blocked summary in event, got %#v", event["summary"])
|
||||
}
|
||||
eventID, ok := event["event_id"].(float64)
|
||||
if !ok {
|
||||
t.Fatalf("expected numeric event_id, got %#v", event["event_id"])
|
||||
}
|
||||
if nextEventID, _ := nestedValue(t, waitResp, "data", "next_event_id").(float64); nextEventID != eventID {
|
||||
t.Fatalf("expected next_event_id %.0f, got %#v", eventID, nestedValue(t, waitResp, "data", "next_event_id"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchWaitRejectsMissingRun verifies wait reports not_found for unknown runs.
|
||||
func TestOrchWaitRejectsMissingRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"wait",
|
||||
"--run", "run_wait_missing_001",
|
||||
)
|
||||
if exitCode != 40 {
|
||||
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "not_found")
|
||||
}
|
||||
|
||||
// TestOrchWaitHelpExplainsBlockingPrimitive verifies wait help explains its blocking role and resume flag.
|
||||
func TestOrchWaitHelpExplainsBlockingPrimitive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand("wait", "--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, "leader-side blocking primitive") {
|
||||
t.Fatalf("expected wait help to explain its role, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "--after-event 0 --timeout-seconds 900") {
|
||||
t.Fatalf("expected wait help to include resume example, got:\n%s", combined)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchWaitPlainTextOutput verifies wait renders matching events in plain-text mode.
|
||||
func TestOrchWaitPlainTextOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_wait_text_001",
|
||||
"--goal", "Validate wait plain-text output",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_wait_text_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_wait_text_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
runInboxCommandEventually(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"update",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
"--status", "blocked",
|
||||
"--summary", "Need deployment window",
|
||||
"--payload-json", `{"question":"Can we ship today?"}`,
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", "run_wait_text_001",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"wait",
|
||||
"--run", "run_wait_text_001",
|
||||
"--for", "task_blocked",
|
||||
"--after-event", "0",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected wait exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "\ttask_blocked\tT1\tNeed deployment window") {
|
||||
t.Fatalf("expected plain-text wait event row, got:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchReconcileAdditionalContracts verifies reconcile missing-run, plain-text, and help behavior.
|
||||
func TestOrchReconcileAdditionalContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("missing run", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", "run_reconcile_missing_001",
|
||||
)
|
||||
if exitCode != 40 {
|
||||
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "not_found")
|
||||
})
|
||||
|
||||
t.Run("plain text", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_reconcile_plain_001",
|
||||
"--goal", "Validate reconcile plain-text output",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_reconcile_plain_001",
|
||||
"--task", "T1",
|
||||
"--title", "Investigate current state",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_reconcile_plain_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"reconcile",
|
||||
"--run", "run_reconcile_plain_001",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected reconcile success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "reconciled run run_reconcile_plain_001 (0 updated tasks)" {
|
||||
t.Fatalf("expected reconcile plain-text summary, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("help", func(t *testing.T) {
|
||||
stdout, stderr, exitCode := executeOrchCommand("reconcile", "--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, "bridge from worker-side inbox activity") {
|
||||
t.Fatalf("expected reconcile help to explain the inbox bridge, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "fresh state") {
|
||||
t.Fatalf("expected reconcile help to mention fresh state, got:\n%s", combined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrchStatusShowsEmptyRunWithoutAttempts verifies status returns an empty task view for untouched runs.
|
||||
func TestOrchStatusShowsEmptyRunWithoutAttempts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_status_empty_001",
|
||||
"--goal", "Inspect untouched run status",
|
||||
)
|
||||
|
||||
statusOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"status",
|
||||
"--run", "run_status_empty_001",
|
||||
)
|
||||
|
||||
var statusResp map[string]any
|
||||
mustDecodeJSON(t, statusOut, &statusResp)
|
||||
if got := nestedString(t, statusResp, "data", "run", "status"); got != "active" {
|
||||
t.Fatalf("expected active run status, got %q", got)
|
||||
}
|
||||
tasks := nestedArray(t, statusResp, "data", "tasks")
|
||||
if len(tasks) != 0 {
|
||||
t.Fatalf("expected no tasks for untouched run, got %#v", tasks)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchWaitBlankFilterFallsBackToDefaultEvents verifies blank wait filters normalize back to the default event set.
|
||||
func TestOrchWaitBlankFilterFallsBackToDefaultEvents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_wait_blank_001",
|
||||
"--goal", "Validate wait filter fallback",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_wait_blank_001",
|
||||
"--task", "T1",
|
||||
"--title", "Ready task",
|
||||
)
|
||||
|
||||
stdout := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"wait",
|
||||
"--run", "run_wait_blank_001",
|
||||
"--for", " , , ",
|
||||
"--after-event", "0",
|
||||
"--timeout-seconds", "1",
|
||||
)
|
||||
|
||||
var waitResp map[string]any
|
||||
mustDecodeJSON(t, stdout, &waitResp)
|
||||
events := nestedArray(t, waitResp, "data", "events")
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("expected one default wait event, got %#v", events)
|
||||
}
|
||||
event, ok := events[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected wait event object, got %#v", events[0])
|
||||
}
|
||||
if got, _ := event["type"].(string); got != "task_ready" {
|
||||
t.Fatalf("expected default task_ready event, got %#v", event["type"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchRecoveryCommandPlainTextOutputs verifies recovery and cleanup commands render human-readable success output.
|
||||
func TestOrchRecoveryCommandPlainTextOutputs(t *testing.T) {
|
||||
t.Run("answer", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
threadID := seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_answer_plain_001", "T2", "worker-b")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"answer",
|
||||
"--run", "run_answer_plain_001",
|
||||
"--task", "T2",
|
||||
"--body", "Use stdout for MVP.",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected answer success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "answered task T2 on thread "+threadID {
|
||||
t.Fatalf("expected answer plain-text output, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("retry", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_retry_plain_001", "T1")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"retry",
|
||||
"--run", "run_retry_plain_001",
|
||||
"--task", "T1",
|
||||
"--body", "Retry with the same execution contract.",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected retry success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "retried task T1 as attempt 2" {
|
||||
t.Fatalf("expected retry plain-text output, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reassign", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_reassign_plain_001", "T1")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"reassign",
|
||||
"--run", "run_reassign_plain_001",
|
||||
"--task", "T1",
|
||||
"--to", "worker-b",
|
||||
"--reason", "Move the retry to another worker.",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected reassign success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "reassigned task T1 to worker-b as attempt 2" {
|
||||
t.Fatalf("expected reassign plain-text output, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cancel", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_cancel_plain_001",
|
||||
"--goal", "Validate cancel plain-text output",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_cancel_plain_001",
|
||||
"--task", "T1",
|
||||
"--title", "Superseded task",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"cancel",
|
||||
"--run", "run_cancel_plain_001",
|
||||
"--task", "T1",
|
||||
"--reason", "Superseded by a new plan.",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected cancel success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "cancelled task T1 in run run_cancel_plain_001" {
|
||||
t.Fatalf("expected cancel plain-text output, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cleanup", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
repoPath := initGitRepo(t)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_cleanup_plain_001",
|
||||
"--goal", "Validate cleanup plain-text output",
|
||||
)
|
||||
seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, "run_cleanup_plain_001", "T1", "worker-a")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"cleanup",
|
||||
"--run", "run_cleanup_plain_001",
|
||||
"--task", "T1",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected cleanup success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "cleaned 1 worktrees" {
|
||||
t.Fatalf("expected cleanup plain-text output, got %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrchRecoveryCommandHelpContracts verifies recovery commands explain their intended operator contract.
|
||||
func TestOrchRecoveryCommandHelpContracts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "answer",
|
||||
args: []string{"answer", "--help"},
|
||||
want: []string{"active blocked attempt thread", "payload-json", "Constraints:"},
|
||||
},
|
||||
{
|
||||
name: "retry",
|
||||
args: []string{"retry", "--help"},
|
||||
want: []string{"fresh attempt and inbox thread", "analysis-only", "Constraints:"},
|
||||
},
|
||||
{
|
||||
name: "reassign",
|
||||
args: []string{"reassign", "--help"},
|
||||
want: []string{"create a new attempt for another worker", "fresh worktree", "Constraints:"},
|
||||
},
|
||||
{
|
||||
name: "cancel",
|
||||
args: []string{"cancel", "--help"},
|
||||
want: []string{"cancel one task inside the run", "entire request", "Constraints:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stdout, stderr, exitCode := executeOrchCommand(tc.args...)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
combined := stdout + stderr
|
||||
for _, want := range tc.want {
|
||||
if !strings.Contains(combined, want) {
|
||||
t.Fatalf("expected %s help to contain %q, got:\n%s", tc.name, want, combined)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyPlainTextOutputs verifies verify record and verify status render human-readable success output.
|
||||
func TestOrchVerifyPlainTextOutputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "run_verify_plain_001"
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, runID, "T1", []string{"lint"})
|
||||
|
||||
recordStdout, recordStderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"verify", "record",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "passed",
|
||||
"--summary", "lint clean",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected verify record success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, recordStderr, recordStdout)
|
||||
}
|
||||
if got := strings.TrimSpace(recordStdout); got != "recorded check lint=passed for "+runID+"/T1 attempt 1" {
|
||||
t.Fatalf("expected verify record plain-text output, got %q", got)
|
||||
}
|
||||
|
||||
statusStdout, statusStderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"verify", "status",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected verify status success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, statusStderr, statusStdout)
|
||||
}
|
||||
if got := strings.TrimSpace(statusStdout); got != runID+"/T1 verification status passed" {
|
||||
t.Fatalf("expected verify status plain-text output, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCouncilAdditionalContracts verifies remaining council help, plain-text, and cancelled-reviewer failure paths.
|
||||
func TestOrchCouncilAdditionalContracts(t *testing.T) {
|
||||
t.Run("wait and tally help", func(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "council wait",
|
||||
args: []string{"council", "wait", "--help"},
|
||||
want: []string{"instead of polling reviewer task state manually", "--timeout-seconds 900", "Constraints:"},
|
||||
},
|
||||
{
|
||||
name: "council tally",
|
||||
args: []string{"council", "tally", "--help"},
|
||||
want: []string{"group similar proposals", "--similarity normal", "Constraints:"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stdout, stderr, exitCode := executeOrchCommand(tc.args...)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
combined := stdout + stderr
|
||||
for _, want := range tc.want {
|
||||
if !strings.Contains(combined, want) {
|
||||
t.Fatalf("expected %s help to contain %q, got:\n%s", tc.name, want, combined)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wait plain text", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_wait_plain_001"
|
||||
seedCouncilRunReadyForTally(t, dbPath, runID)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"council", "wait",
|
||||
"--run", runID,
|
||||
"--timeout-seconds", "1",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected council wait success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if got := strings.TrimSpace(stdout); got != "all council reviewers completed for run "+runID {
|
||||
t.Fatalf("expected council wait plain-text output, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tally plain text", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_tally_plain_001"
|
||||
seedCouncilRunReadyForTally(t, dbPath, runID)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"council", "tally",
|
||||
"--run", runID,
|
||||
"--similarity", "normal",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected council tally success, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if !strings.Contains(strings.TrimSpace(stdout), "tallied council run "+runID+" into ") {
|
||||
t.Fatalf("expected council tally plain-text output, got %q", stdout)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cancelled reviewer rejected by tally", func(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_tally_cancelled_001"
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", runID,
|
||||
"--target", "Review cancelled reviewer handling.",
|
||||
)
|
||||
completeCouncilReviewer(t, dbPath, runID, "architecture-reviewer", councilReviewerBody("architecture-reviewer", "Keep terminal states explicit", "Terminal states should stay visible.", "Preserve reviewer terminal states in council workflows."))
|
||||
completeCouncilReviewer(t, dbPath, runID, "implementation-reviewer", councilReviewerBody("implementation-reviewer", "Reject incomplete tally inputs", "Tally should require successful reviewers.", "Reject reviewer sets that are not all done."))
|
||||
cancelCouncilReviewerForAdditionalTests(t, dbPath, runID, "risk-reviewer", "Risk review superseded")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "tally",
|
||||
"--run", runID,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_state exit 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_state")
|
||||
assertErrorMessageContains(t, stdout, "did not finish successfully")
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrchCouncilReportArtifactPathUsesExpectedBaseDir verifies council report artifacts derive from the intended base directory.
|
||||
func TestOrchCouncilReportArtifactPathUsesExpectedBaseDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
dbPath string
|
||||
runID string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "current directory db",
|
||||
dbPath: ".agents/coord.db",
|
||||
runID: "council_blog_001",
|
||||
want: filepath.Join(".orch", "reports", "council_blog_001.md"),
|
||||
},
|
||||
{
|
||||
name: "db outside .agents",
|
||||
dbPath: filepath.Join("/tmp", "coord", "coord.db"),
|
||||
runID: "council_blog_002",
|
||||
want: filepath.Join("/tmp", "coord", ".orch", "reports", "council_blog_002.md"),
|
||||
},
|
||||
{
|
||||
name: "sanitize run id separators",
|
||||
dbPath: filepath.Join("/tmp", "project", ".agents", "coord.db"),
|
||||
runID: filepath.Join("council", "blog", "003"),
|
||||
want: filepath.Join("/tmp", "project", ".orch", "reports", "council_blog_003.md"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := councilReportArtifactPath(tc.dbPath, tc.runID); got != tc.want {
|
||||
t.Fatalf("expected council report path %q, got %q", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func seedCouncilRunReadyForTally(t *testing.T, dbPath, runID string) {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", runID,
|
||||
"--target", "Review the council plain-text flow.",
|
||||
)
|
||||
completeCouncilReviewer(t, dbPath, runID, "architecture-reviewer", councilReviewerBody("architecture-reviewer", "Split contracts", "Transport contracts are mixed into UI code.", "Move API contract definitions into a dedicated module."))
|
||||
completeCouncilReviewer(t, dbPath, runID, "implementation-reviewer", councilReviewerBody("implementation-reviewer", "Extract contracts", "Shared transport shapes are duplicated.", "Move API contract definitions into a dedicated module."))
|
||||
completeCouncilReviewer(t, dbPath, runID, "risk-reviewer", councilReviewerBody("risk-reviewer", "Cover regressions", "Contract drift becomes risky over time.", "Add integration tests for the council report flow."))
|
||||
}
|
||||
|
||||
func councilReviewerBody(role, title, summary, proposal string) string {
|
||||
return fmt.Sprintf(
|
||||
`{"reviewer_role":%q,"findings":[{"title":%q,"summary":%q,"proposal":%q,"rationale":"Keep the council workflow deterministic.","confidence":"high","tags":["contracts"],"target_refs":{"repo_path":"."}}]}`,
|
||||
role,
|
||||
title,
|
||||
summary,
|
||||
proposal,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchBlockedPlainTextOutput verifies blocked renders tabular output in plain-text mode.
|
||||
func TestOrchBlockedPlainTextOutput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
_ = seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_blocked_plain_001", "T2", "worker-b")
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"blocked",
|
||||
"--run", "run_blog_blocked_plain_001",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected plain blocked exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "T2\tNeed logging decision\n") {
|
||||
t.Fatalf("expected blocked plain output to include task summary, got:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchBlockedPlainTextNoBlockedTasks verifies blocked reports the empty queue in plain-text mode.
|
||||
func TestOrchBlockedPlainTextNoBlockedTasks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_blocked_plain_002",
|
||||
"--goal", "Validate empty blocked queue",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"blocked",
|
||||
"--run", "run_blog_blocked_plain_002",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected empty blocked exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if stdout != "no blocked tasks\n" {
|
||||
t.Fatalf("expected no blocked tasks line, got %#v", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchAnswerRejectsTerminalThread verifies answer rejects blocked tasks whose active thread is already terminal.
|
||||
func TestOrchAnswerRejectsTerminalThread(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
threadID := seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_terminal_001", "T2", "worker-b")
|
||||
setThreadStatusForRecoveryCleanupTests(t, dbPath, threadID, "cancelled")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"answer",
|
||||
"--run", "run_blog_answer_terminal_001",
|
||||
"--task", "T2",
|
||||
"--body", "Use stdout.",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_state exit 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_state")
|
||||
assertErrorMessageContains(t, stdout, "already terminal")
|
||||
}
|
||||
|
||||
// TestOrchAnswerRejectsBodyAndBodyFileTogether verifies answer enforces body/body-file mutual exclusion.
|
||||
func TestOrchAnswerRejectsBodyAndBodyFileTogether(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
bodyFile := filepath.Join(tempDir, "answer.txt")
|
||||
if err := os.WriteFile(bodyFile, []byte("Use stdout.\n"), 0o644); err != nil {
|
||||
t.Fatalf("write answer body file: %v", err)
|
||||
}
|
||||
_ = seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_body_guard_001", "T2", "worker-b")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"answer",
|
||||
"--run", "run_blog_answer_body_guard_001",
|
||||
"--task", "T2",
|
||||
"--body", "Use stdout.",
|
||||
"--body-file", bodyFile,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "mutually exclusive")
|
||||
}
|
||||
|
||||
// TestOrchAnswerRejectsUnreadableBodyFile verifies answer surfaces unreadable body-file errors.
|
||||
func TestOrchAnswerRejectsUnreadableBodyFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
_ = seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_body_guard_002", "T2", "worker-b")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"answer",
|
||||
"--run", "run_blog_answer_body_guard_002",
|
||||
"--task", "T2",
|
||||
"--body-file", filepath.Join(tempDir, "missing-answer.txt"),
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "failed to read body-file")
|
||||
}
|
||||
|
||||
// TestOrchRetryOverridesAssignedWorkerAndAbandonsPreviousWorktree verifies retry can retarget a code attempt and mark the old worktree abandoned.
|
||||
func TestOrchRetryOverridesAssignedWorkerAndAbandonsPreviousWorktree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
_, originalWorktreePath := seedFailedCodeModeTaskForRecoveryCleanupTests(t, dbPath, "run_blog_retry_extra_001", "T1", "worker-a")
|
||||
|
||||
retryOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", "run_blog_retry_extra_001",
|
||||
"--task", "T1",
|
||||
"--to", "worker-b",
|
||||
"--body", "Retry with a different worker.",
|
||||
)
|
||||
|
||||
var retryResp map[string]any
|
||||
mustDecodeJSON(t, retryOut, &retryResp)
|
||||
if got := nestedString(t, retryResp, "data", "attempt", "assigned_to"); got != "worker-b" {
|
||||
t.Fatalf("expected retry target worker-b, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, retryResp, "data", "previous_attempt", "worktree_path"); got != originalWorktreePath {
|
||||
t.Fatalf("expected previous attempt worktree %q, got %q", originalWorktreePath, got)
|
||||
}
|
||||
newWorktreePath := nestedString(t, retryResp, "data", "attempt", "worktree_path")
|
||||
if newWorktreePath == "" || newWorktreePath == originalWorktreePath {
|
||||
t.Fatalf("expected retry to create a fresh worktree, got %q", newWorktreePath)
|
||||
}
|
||||
if _, err := os.Stat(newWorktreePath); err != nil {
|
||||
t.Fatalf("stat retry worktree %s: %v", newWorktreePath, err)
|
||||
}
|
||||
|
||||
status, workspaceStatus, worktreePath := queryAttemptStateForRecoveryCleanupTests(t, dbPath, "run_blog_retry_extra_001", "T1", 1)
|
||||
if status != "failed" {
|
||||
t.Fatalf("expected previous attempt status failed, got %q", status)
|
||||
}
|
||||
if workspaceStatus != "abandoned" {
|
||||
t.Fatalf("expected previous workspace_status abandoned, got %q", workspaceStatus)
|
||||
}
|
||||
if worktreePath != originalWorktreePath {
|
||||
t.Fatalf("expected original worktree path %q, got %q", originalWorktreePath, worktreePath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchRetryAnalysisModeRemainsWithoutWorktree verifies retry preserves analysis-mode attempts without allocating a worktree.
|
||||
func TestOrchRetryAnalysisModeRemainsWithoutWorktree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
originalThreadID := seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_blog_retry_extra_002", "T1")
|
||||
|
||||
retryOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", "run_blog_retry_extra_002",
|
||||
"--task", "T1",
|
||||
"--body", "Retry analysis-only work.",
|
||||
)
|
||||
|
||||
var retryResp map[string]any
|
||||
mustDecodeJSON(t, retryOut, &retryResp)
|
||||
attempt, ok := nestedValue(t, retryResp, "data", "attempt").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected retry attempt object, got %#v", nestedValue(t, retryResp, "data", "attempt"))
|
||||
}
|
||||
if got, _ := attempt["thread_id"].(string); got == originalThreadID {
|
||||
t.Fatalf("expected retry to create a new thread, got %q", got)
|
||||
}
|
||||
if got, _ := attempt["worktree_path"].(string); got != "" {
|
||||
t.Fatalf("expected no worktree_path for analysis retry, got %q", got)
|
||||
}
|
||||
if got, _ := attempt["workspace_status"].(string); got != "" {
|
||||
t.Fatalf("expected no workspace_status for analysis retry, got %q", got)
|
||||
}
|
||||
|
||||
status, workspaceStatus, worktreePath := queryAttemptStateForRecoveryCleanupTests(t, dbPath, "run_blog_retry_extra_002", "T1", 2)
|
||||
if status != "dispatched" {
|
||||
t.Fatalf("expected retry attempt status dispatched, got %q", status)
|
||||
}
|
||||
if workspaceStatus != "" {
|
||||
t.Fatalf("expected empty workspace_status for analysis retry, got %q", workspaceStatus)
|
||||
}
|
||||
if worktreePath != "" {
|
||||
t.Fatalf("expected empty worktree_path for analysis retry, got %q", worktreePath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchReassignCodeModeCreatesFreshWorktree verifies reassign creates a new code worktree and abandons the previous one.
|
||||
func TestOrchReassignCodeModeCreatesFreshWorktree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
_, originalWorktreePath := seedBlockedCodeModeTaskForRecoveryCleanupTests(t, dbPath, "run_blog_reassign_extra_001", "T1", "worker-a")
|
||||
|
||||
reassignOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reassign",
|
||||
"--run", "run_blog_reassign_extra_001",
|
||||
"--task", "T1",
|
||||
"--to", "worker-b",
|
||||
"--reason", "Move to another worker.",
|
||||
)
|
||||
|
||||
var reassignResp map[string]any
|
||||
mustDecodeJSON(t, reassignOut, &reassignResp)
|
||||
if got := nestedString(t, reassignResp, "data", "previous_attempt", "worktree_path"); got != originalWorktreePath {
|
||||
t.Fatalf("expected previous attempt worktree %q, got %q", originalWorktreePath, got)
|
||||
}
|
||||
newWorktreePath := nestedString(t, reassignResp, "data", "attempt", "worktree_path")
|
||||
if newWorktreePath == "" || newWorktreePath == originalWorktreePath {
|
||||
t.Fatalf("expected reassignment to create a fresh worktree, got %q", newWorktreePath)
|
||||
}
|
||||
if _, err := os.Stat(newWorktreePath); err != nil {
|
||||
t.Fatalf("stat reassigned worktree %s: %v", newWorktreePath, err)
|
||||
}
|
||||
|
||||
status, workspaceStatus, _ := queryAttemptStateForRecoveryCleanupTests(t, dbPath, "run_blog_reassign_extra_001", "T1", 1)
|
||||
if status != "cancelled" {
|
||||
t.Fatalf("expected previous attempt status cancelled, got %q", status)
|
||||
}
|
||||
if workspaceStatus != "abandoned" {
|
||||
t.Fatalf("expected previous workspace_status abandoned, got %q", workspaceStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCancelRejectsAlreadyCancelledTask verifies cancel rejects a task that was already cancelled explicitly.
|
||||
func TestOrchCancelRejectsAlreadyCancelledTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_cancel_extra_001",
|
||||
"--goal", "Validate repeated task cancellation",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_cancel_extra_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement backend",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cancel",
|
||||
"--run", "run_blog_cancel_extra_001",
|
||||
"--task", "T1",
|
||||
"--reason", "Task no longer needed.",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cancel",
|
||||
"--run", "run_blog_cancel_extra_001",
|
||||
"--task", "T1",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_state exit 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_state")
|
||||
assertErrorMessageContains(t, stdout, "already cancelled")
|
||||
}
|
||||
|
||||
// TestOrchCancelCodeModeMarksWorkspaceAbandoned verifies cancel leaves code-mode attempts as abandoned until cleanup removes them.
|
||||
func TestOrchCancelCodeModeMarksWorkspaceAbandoned(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
threadID, worktreePath := seedCodeModeDispatchedTaskForRecoveryCleanupTests(t, dbPath, "run_blog_cancel_extra_002", "T1", "worker-a")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cancel",
|
||||
"--run", "run_blog_cancel_extra_002",
|
||||
"--task", "T1",
|
||||
"--reason", "Stop this worktree attempt.",
|
||||
)
|
||||
|
||||
status, workspaceStatus, gotWorktreePath := queryAttemptStateForRecoveryCleanupTests(t, dbPath, "run_blog_cancel_extra_002", "T1", 1)
|
||||
if status != "cancelled" {
|
||||
t.Fatalf("expected cancelled attempt status, got %q", status)
|
||||
}
|
||||
if workspaceStatus != "abandoned" {
|
||||
t.Fatalf("expected abandoned workspace_status, got %q", workspaceStatus)
|
||||
}
|
||||
if gotWorktreePath != worktreePath {
|
||||
t.Fatalf("expected worktree_path %q, got %q", worktreePath, gotWorktreePath)
|
||||
}
|
||||
if _, err := os.Stat(worktreePath); err != nil {
|
||||
t.Fatalf("expected cancelled worktree to remain for later cleanup, err=%v", err)
|
||||
}
|
||||
|
||||
showOut := runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"show",
|
||||
"--thread", threadID,
|
||||
)
|
||||
|
||||
var showResp map[string]any
|
||||
mustDecodeJSON(t, showOut, &showResp)
|
||||
if got := nestedString(t, showResp, "data", "thread", "status"); got != "cancelled" {
|
||||
t.Fatalf("expected cancelled inbox thread, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCleanupRejectsWithoutSelector verifies cleanup requires an explicit selector.
|
||||
func TestOrchCleanupRejectsWithoutSelector(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_cleanup_extra_001",
|
||||
"--goal", "Validate cleanup selector requirements",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cleanup",
|
||||
"--run", "run_blog_cleanup_extra_001",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "specify --task, --attempt, or --all-completed")
|
||||
}
|
||||
|
||||
// TestOrchCleanupForceRemovesActiveWorktree verifies cleanup can force-remove a created worktree that is not yet terminal.
|
||||
func TestOrchCleanupForceRemovesActiveWorktree(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
_, worktreePath := seedCodeModeDispatchedTaskForRecoveryCleanupTests(t, dbPath, "run_blog_cleanup_extra_002", "T1", "worker-a")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cleanup",
|
||||
"--run", "run_blog_cleanup_extra_002",
|
||||
"--task", "T1",
|
||||
"--attempt", "1",
|
||||
)
|
||||
if exitCode != 10 {
|
||||
t.Fatalf("expected no_matching_work exit 10 without force, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "no_matching_work")
|
||||
|
||||
cleanupOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cleanup",
|
||||
"--run", "run_blog_cleanup_extra_002",
|
||||
"--task", "T1",
|
||||
"--attempt", "1",
|
||||
"--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 forced attempt, got %#v", cleaned)
|
||||
}
|
||||
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected forced cleanup worktree path to be removed, err=%v", err)
|
||||
}
|
||||
|
||||
_, workspaceStatus, _ := queryAttemptStateForRecoveryCleanupTests(t, dbPath, "run_blog_cleanup_extra_002", "T1", 1)
|
||||
if workspaceStatus != "cleaned" {
|
||||
t.Fatalf("expected cleaned workspace_status after force cleanup, got %q", workspaceStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCleanupRemovesAbandonedWorktreeByExactAttempt verifies cleanup can target one abandoned attempt without touching the replacement worktree.
|
||||
func TestOrchCleanupRemovesAbandonedWorktreeByExactAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
_, originalWorktreePath := seedFailedCodeModeTaskForRecoveryCleanupTests(t, dbPath, "run_blog_cleanup_extra_003", "T1", "worker-a")
|
||||
|
||||
retryOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", "run_blog_cleanup_extra_003",
|
||||
"--task", "T1",
|
||||
"--body", "Retry in a fresh worktree.",
|
||||
)
|
||||
|
||||
var retryResp map[string]any
|
||||
mustDecodeJSON(t, retryOut, &retryResp)
|
||||
replacementWorktreePath := nestedString(t, retryResp, "data", "attempt", "worktree_path")
|
||||
if replacementWorktreePath == "" || replacementWorktreePath == originalWorktreePath {
|
||||
t.Fatalf("expected retry to create a replacement worktree, got %q", replacementWorktreePath)
|
||||
}
|
||||
|
||||
cleanupOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cleanup",
|
||||
"--run", "run_blog_cleanup_extra_003",
|
||||
"--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 abandoned 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["attempt_no"].(float64); got != 1 {
|
||||
t.Fatalf("expected cleaned attempt 1, got %#v", cleanedAttempt["attempt_no"])
|
||||
}
|
||||
if _, err := os.Stat(originalWorktreePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected original abandoned worktree to be removed, err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(replacementWorktreePath); err != nil {
|
||||
t.Fatalf("expected replacement worktree to remain, err=%v", err)
|
||||
}
|
||||
|
||||
_, workspaceStatus, _ := queryAttemptStateForRecoveryCleanupTests(t, dbPath, "run_blog_cleanup_extra_003", "T1", 1)
|
||||
if workspaceStatus != "cleaned" {
|
||||
t.Fatalf("expected cleaned workspace_status for attempt 1, got %q", workspaceStatus)
|
||||
}
|
||||
}
|
||||
|
||||
func seedCodeModeDispatchedTaskForRecoveryCleanupTests(t *testing.T, dbPath, runID, taskID, agent string) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
repoPath := initGitRepo(t)
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Prepare code-mode task for recovery and cleanup tests",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", "Prepare code-mode task",
|
||||
"--default-to", agent,
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--execution-mode", "code",
|
||||
"--repo-path", repoPath,
|
||||
"--workspace-root", ".orch/worktrees",
|
||||
"--to", agent,
|
||||
"--body", "Prepare isolated worktree.",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
return nestedString(t, dispatchResp, "data", "attempt", "thread_id"), nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
|
||||
}
|
||||
|
||||
func seedFailedCodeModeTaskForRecoveryCleanupTests(t *testing.T, dbPath, runID, taskID, agent string) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
threadID, worktreePath := seedCodeModeDispatchedTaskForRecoveryCleanupTests(t, dbPath, runID, taskID, agent)
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", agent,
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"fail",
|
||||
"--agent", agent,
|
||||
"--thread", threadID,
|
||||
"--summary", "Code attempt failed",
|
||||
"--body", "The code attempt failed before completion.",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
|
||||
return threadID, worktreePath
|
||||
}
|
||||
|
||||
func seedBlockedCodeModeTaskForRecoveryCleanupTests(t *testing.T, dbPath, runID, taskID, agent string) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
threadID, worktreePath := seedCodeModeDispatchedTaskForRecoveryCleanupTests(t, dbPath, runID, taskID, agent)
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", agent,
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"update",
|
||||
"--agent", agent,
|
||||
"--thread", threadID,
|
||||
"--status", "blocked",
|
||||
"--summary", "Need code review decision",
|
||||
"--payload-json", `{"question":"Proceed with the current worktree?"}`,
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
|
||||
return threadID, worktreePath
|
||||
}
|
||||
|
||||
func queryAttemptStateForRecoveryCleanupTests(t *testing.T, dbPath, runID, taskID string, attemptNo int) (string, string, string) {
|
||||
t.Helper()
|
||||
|
||||
sqlDB, err := openOrchDB(context.Background(), dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open orch db: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
row := sqlDB.QueryRowContext(
|
||||
context.Background(),
|
||||
`SELECT status, COALESCE(workspace_status, ''), COALESCE(worktree_path, '')
|
||||
FROM task_attempts
|
||||
WHERE run_id = ? AND task_id = ? AND attempt_no = ?`,
|
||||
runID,
|
||||
taskID,
|
||||
attemptNo,
|
||||
)
|
||||
|
||||
var status string
|
||||
var workspaceStatus string
|
||||
var worktreePath string
|
||||
if err := row.Scan(&status, &workspaceStatus, &worktreePath); err != nil {
|
||||
t.Fatalf("scan attempt state for %s/%s/%d: %v", runID, taskID, attemptNo, err)
|
||||
}
|
||||
return status, workspaceStatus, worktreePath
|
||||
}
|
||||
|
||||
func setThreadStatusForRecoveryCleanupTests(t *testing.T, dbPath, threadID, status string) {
|
||||
t.Helper()
|
||||
|
||||
sqlDB, err := openOrchDB(context.Background(), dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open orch db: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
if _, err := sqlDB.ExecContext(context.Background(), `UPDATE threads SET status = ? WHERE thread_id = ?`, status, threadID); err != nil {
|
||||
t.Fatalf("update thread %s status to %s: %v", threadID, status, err)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,16 @@ package orch
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var orchCommandDirMu sync.Mutex
|
||||
|
||||
func runOrchCommand(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
|
||||
@@ -27,6 +31,35 @@ func executeOrchCommand(args ...string) (string, string, int) {
|
||||
return stdout.String(), stderr.String(), exitCode
|
||||
}
|
||||
|
||||
func executeOrchCommandInDir(dir string, args ...string) (string, string, int) {
|
||||
orchCommandDirMu.Lock()
|
||||
defer orchCommandDirMu.Unlock()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err.Error(), 1
|
||||
}
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
return "", err.Error(), 1
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(cwd); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
return executeOrchCommand(args...)
|
||||
}
|
||||
|
||||
func orchCommandPath() string {
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("unable to determine orch test helper path")
|
||||
}
|
||||
|
||||
return filepath.Join(filepath.Dir(file), "..", "..", "..", "cmd", "orch")
|
||||
}
|
||||
|
||||
func runInboxCommand(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchVerifyRecordRejectsInvalidStatus verifies verify record rejects unsupported check statuses.
|
||||
func TestOrchVerifyRecordRejectsInvalidStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_additional_001", "T1", []string{"lint"})
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_additional_001",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "maybe",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "check status must be one of passed, failed, skipped")
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordRejectsInvalidMetadataJSON verifies verify record rejects malformed metadata JSON.
|
||||
func TestOrchVerifyRecordRejectsInvalidMetadataJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_additional_002", "T1", []string{"lint"})
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_additional_002",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "passed",
|
||||
"--metadata-json", `{"trace":`,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "metadata-json must be valid JSON")
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordRejectsNonLatestAttempt verifies verify record only accepts the latest attempt.
|
||||
func TestOrchVerifyRecordRejectsNonLatestAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "run_verify_additional_003"
|
||||
seedVerifyingTaskForVerifyContractTests(t, dbPath, runID, "T1")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "failed",
|
||||
"--summary", "lint failed on attempt 1",
|
||||
)
|
||||
|
||||
retryOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--body", "Retry after fixing lint failures.",
|
||||
)
|
||||
|
||||
var retryResp map[string]any
|
||||
mustDecodeJSON(t, retryOut, &retryResp)
|
||||
threadID := nestedString(t, retryResp, "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", "Attempt 2 complete",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--attempt", "1",
|
||||
"--check", "lint",
|
||||
"--status", "passed",
|
||||
)
|
||||
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, "latest attempt")
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordRejectsTaskWithoutAttempt verifies verify record rejects tasks that were never dispatched.
|
||||
func TestOrchVerifyRecordRejectsTaskWithoutAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_verify_additional_004",
|
||||
"--goal", "Seed task without attempts",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_verify_additional_004",
|
||||
"--task", "T1",
|
||||
"--title", "Undispatched task",
|
||||
"--required-check", "lint",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_additional_004",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "passed",
|
||||
)
|
||||
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, "has no attempt to verify")
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordSkippedCheckKeepsGatePending verifies skipped checks remain pending instead of completing the gate.
|
||||
func TestOrchVerifyRecordSkippedCheckKeepsGatePending(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_additional_005", "T1", []string{"lint"})
|
||||
|
||||
recordOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_additional_005",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "skipped",
|
||||
"--summary", "lint intentionally skipped",
|
||||
)
|
||||
|
||||
var recordResp map[string]any
|
||||
mustDecodeJSON(t, recordOut, &recordResp)
|
||||
if got := nestedString(t, recordResp, "data", "check", "status"); got != "skipped" {
|
||||
t.Fatalf("expected skipped check status, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, recordResp, "data", "task", "status"); got != "verifying" {
|
||||
t.Fatalf("expected task to stay verifying, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, recordResp, "data", "gate", "status"); got != "pending" {
|
||||
t.Fatalf("expected pending gate after skipped check, got %q", got)
|
||||
}
|
||||
pendingChecks := nestedArray(t, recordResp, "data", "gate", "pending_checks")
|
||||
if len(pendingChecks) != 1 || pendingChecks[0] != "lint" {
|
||||
t.Fatalf("expected lint to remain pending, got %#v", pendingChecks)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyStatusReportsNoGateInPlainText verifies verify status prints a no-gate message for ungated tasks.
|
||||
func TestOrchVerifyStatusReportsNoGateInPlainText(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_verify_additional_006",
|
||||
"--goal", "Seed ungated verify status task",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_verify_additional_006",
|
||||
"--task", "T1",
|
||||
"--title", "Ungated task",
|
||||
)
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"verify", "status",
|
||||
"--run", "run_verify_additional_006",
|
||||
"--task", "T1",
|
||||
)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected verify status exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
if !strings.Contains(stdout, "run_verify_additional_006/T1 has no verification gate") {
|
||||
t.Fatalf("expected no-gate plain-text output, got:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyStatusHelpExplainsGateInspection verifies verify status help explains gate inspection scope.
|
||||
func TestOrchVerifyStatusHelpExplainsGateInspection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand("verify", "status", "--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, "task spec snapshot") {
|
||||
t.Fatalf("expected verify status help to mention the task spec snapshot, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "stuck in verifying or failed") {
|
||||
t.Fatalf("expected verify status help to explain verifying/failed inspection, got:\n%s", combined)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCouncilStartRejectsInvalidInputs verifies council start rejects missing targets and invalid enum values.
|
||||
func TestOrchCouncilStartRejectsInvalidInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
message string
|
||||
}{
|
||||
{
|
||||
name: "missing target",
|
||||
args: []string{
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_invalid_missing_target_001",
|
||||
},
|
||||
message: "at least one of target, target-file, repo-path, or task-id is required",
|
||||
},
|
||||
{
|
||||
name: "invalid mode",
|
||||
args: []string{
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_invalid_mode_001",
|
||||
"--target", "Review the current design.",
|
||||
"--mode", "plan",
|
||||
},
|
||||
message: "mode must be brainstorm or review",
|
||||
},
|
||||
{
|
||||
name: "invalid target type",
|
||||
args: []string{
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_invalid_target_type_001",
|
||||
"--target", "Review the current design.",
|
||||
"--target-type", "doc",
|
||||
},
|
||||
message: "target-type must be text, repo, or mixed",
|
||||
},
|
||||
{
|
||||
name: "invalid output",
|
||||
args: []string{
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_invalid_output_001",
|
||||
"--target", "Review the current design.",
|
||||
"--output", "html",
|
||||
},
|
||||
message: "output must be markdown, json, or both",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stdout, _, exitCode := executeOrchCommand(tc.args...)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, tc.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCouncilStartRejectsDuplicateRun verifies council start refuses to reuse an existing run ID.
|
||||
func TestOrchCouncilStartRejectsDuplicateRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_duplicate_run_001",
|
||||
"--target", "Review the current design.",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_duplicate_run_001",
|
||||
"--target", "Review the current design again.",
|
||||
)
|
||||
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, "already exists")
|
||||
}
|
||||
|
||||
// TestOrchCouncilWaitTreatsFailedAndCancelledReviewersAsComplete verifies council wait wakes once reviewers are terminal, even when some failed or were cancelled.
|
||||
func TestOrchCouncilWaitTreatsFailedAndCancelledReviewersAsComplete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_wait_terminals_001"
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", runID,
|
||||
"--target", "Review the council workflow terminals.",
|
||||
)
|
||||
|
||||
completeCouncilReviewer(t, dbPath, runID, "architecture-reviewer", `{"reviewer_role":"architecture-reviewer","findings":[{"title":"Keep queue small","summary":"One reviewer completed successfully.","proposal":"Keep the council wait contract explicit.","rationale":"This protects terminal-state handling.","confidence":"high","tags":["workflow"],"target_refs":{"repo_path":"."}}]}`)
|
||||
failCouncilReviewerForAdditionalTests(t, dbPath, runID, "implementation-reviewer", "Implementation review failed")
|
||||
cancelCouncilReviewerForAdditionalTests(t, dbPath, runID, "risk-reviewer", "Risk review superseded")
|
||||
|
||||
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_complete true, got %#v", waitResp)
|
||||
}
|
||||
statuses := reviewerStatusesByRole(t, waitResp)
|
||||
if statuses["architecture-reviewer"] != "done" || statuses["implementation-reviewer"] != "failed" || statuses["risk-reviewer"] != "cancelled" {
|
||||
t.Fatalf("unexpected reviewer statuses: %#v", statuses)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCouncilTallyRejectsInvalidSimilarity verifies council tally rejects unsupported similarity modes.
|
||||
func TestOrchCouncilTallyRejectsInvalidSimilarity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "tally",
|
||||
"--run", "council_invalid_similarity_001",
|
||||
"--similarity", "loose",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "similarity must be strict or normal")
|
||||
}
|
||||
|
||||
// TestOrchCouncilTallyRejectsUnfinishedReviewers verifies council tally refuses runs with incomplete reviewer work.
|
||||
func TestOrchCouncilTallyRejectsUnfinishedReviewers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_tally_unfinished_001",
|
||||
"--target", "Review unfinished council behavior.",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "tally",
|
||||
"--run", "council_tally_unfinished_001",
|
||||
)
|
||||
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, "not complete yet")
|
||||
}
|
||||
|
||||
// TestOrchCouncilTallyRejectsFailedReviewer verifies council tally refuses reviewer sets that did not all finish successfully.
|
||||
func TestOrchCouncilTallyRejectsFailedReviewer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_tally_failed_001"
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", runID,
|
||||
"--target", "Review failed reviewer handling.",
|
||||
)
|
||||
|
||||
completeCouncilReviewer(t, dbPath, runID, "architecture-reviewer", `{"reviewer_role":"architecture-reviewer","findings":[{"title":"Use explicit gates","summary":"Architecture reviewer completed.","proposal":"Keep council tally strict about success.","rationale":"This avoids mixed-quality reports.","confidence":"high","tags":["architecture"],"target_refs":{"repo_path":"."}}]}`)
|
||||
completeCouncilReviewer(t, dbPath, runID, "risk-reviewer", `{"reviewer_role":"risk-reviewer","findings":[{"title":"Keep failure visible","summary":"Risk reviewer completed.","proposal":"Preserve terminal reviewer status.","rationale":"Operators need the final state.","confidence":"medium","tags":["risk"],"target_refs":{"repo_path":"."}}]}`)
|
||||
failCouncilReviewerForAdditionalTests(t, dbPath, runID, "implementation-reviewer", "Implementation reviewer could not finish")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "tally",
|
||||
"--run", runID,
|
||||
)
|
||||
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, "did not finish successfully")
|
||||
}
|
||||
|
||||
// TestOrchCouncilTallyRejectsInvalidReviewerJSON verifies council tally rejects reviewer result bodies that are not JSON.
|
||||
func TestOrchCouncilTallyRejectsInvalidReviewerJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_tally_invalid_json_001"
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", runID,
|
||||
"--target", "Review invalid council reviewer JSON.",
|
||||
)
|
||||
|
||||
completeCouncilReviewer(t, dbPath, runID, "architecture-reviewer", `{"reviewer_role":"architecture-reviewer","findings":[{"title":"Keep it valid","summary":"Architecture reviewer completed.","proposal":"Use valid JSON payloads.","rationale":"The tally step requires structured input.","confidence":"high","tags":["contracts"],"target_refs":{"repo_path":"."}}]}`)
|
||||
completeCouncilReviewer(t, dbPath, runID, "risk-reviewer", `{"reviewer_role":"risk-reviewer","findings":[{"title":"Validate inputs","summary":"Risk reviewer completed.","proposal":"Reject malformed reviewer payloads.","rationale":"This keeps the council run deterministic.","confidence":"high","tags":["risk"],"target_refs":{"repo_path":"."}}]}`)
|
||||
completeCouncilReviewer(t, dbPath, runID, "implementation-reviewer", "not-json")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "tally",
|
||||
"--run", runID,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "reviewer output must be valid JSON")
|
||||
}
|
||||
|
||||
// TestOrchCouncilTallyRejectsReviewerRoleMismatch verifies council tally rejects reviewer outputs with mismatched reviewer_role values.
|
||||
func TestOrchCouncilTallyRejectsReviewerRoleMismatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "council_tally_role_mismatch_001"
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", runID,
|
||||
"--target", "Review council reviewer role mismatches.",
|
||||
)
|
||||
|
||||
completeCouncilReviewer(t, dbPath, runID, "architecture-reviewer", `{"reviewer_role":"architecture-reviewer","findings":[{"title":"Match reviewer roles","summary":"Architecture reviewer completed.","proposal":"Keep reviewer metadata aligned.","rationale":"The tally step groups by reviewer role.","confidence":"medium","tags":["contracts"],"target_refs":{"repo_path":"."}}]}`)
|
||||
completeCouncilReviewer(t, dbPath, runID, "risk-reviewer", `{"reviewer_role":"risk-reviewer","findings":[{"title":"Keep roles explicit","summary":"Risk reviewer completed.","proposal":"Validate reviewer_role during tally.","rationale":"This blocks malformed council output.","confidence":"high","tags":["risk"],"target_refs":{"repo_path":"."}}]}`)
|
||||
completeCouncilReviewer(t, dbPath, runID, "implementation-reviewer", `{"reviewer_role":"risk-reviewer","findings":[{"title":"Wrong role","summary":"Role intentionally mismatched.","proposal":"Reject this output.","rationale":"The implementation reviewer should not impersonate another reviewer.","confidence":"high","tags":["contracts"],"target_refs":{"repo_path":"."}}]}`)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "tally",
|
||||
"--run", runID,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
assertErrorMessageContains(t, stdout, "does not match expected implementation-reviewer")
|
||||
}
|
||||
|
||||
func failCouncilReviewerForAdditionalTests(t *testing.T, dbPath, runID, reviewerRole, summary string) {
|
||||
t.Helper()
|
||||
|
||||
threadID := councilReviewerThreadIDForAdditionalTests(t, dbPath, runID, reviewerRole)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", reviewerRole,
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"fail",
|
||||
"--agent", reviewerRole,
|
||||
"--thread", threadID,
|
||||
"--summary", summary,
|
||||
)
|
||||
}
|
||||
|
||||
func cancelCouncilReviewerForAdditionalTests(t *testing.T, dbPath, runID, reviewerRole, reason string) {
|
||||
t.Helper()
|
||||
|
||||
threadID := councilReviewerThreadIDForAdditionalTests(t, dbPath, runID, reviewerRole)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cancel",
|
||||
"--agent", "leader",
|
||||
"--thread", threadID,
|
||||
"--reason", reason,
|
||||
)
|
||||
}
|
||||
|
||||
func councilReviewerThreadIDForAdditionalTests(t *testing.T, dbPath, runID, reviewerRole string) string {
|
||||
t.Helper()
|
||||
|
||||
sqlDB, err := openOrchDB(t.Context(), dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open orch db: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
var threadID string
|
||||
if err := sqlDB.QueryRowContext(
|
||||
t.Context(),
|
||||
`SELECT a.thread_id
|
||||
FROM council_reviewers cr
|
||||
JOIN task_attempts a
|
||||
ON a.run_id = cr.run_id
|
||||
AND a.task_id = cr.task_id
|
||||
AND a.attempt_no = 1
|
||||
WHERE cr.run_id = ? AND cr.reviewer_role = ?`,
|
||||
runID,
|
||||
reviewerRole,
|
||||
).Scan(&threadID); err != nil {
|
||||
t.Fatalf("query council reviewer thread for %s: %v", reviewerRole, err)
|
||||
}
|
||||
|
||||
return threadID
|
||||
}
|
||||
|
||||
func reviewerStatusesByRole(t *testing.T, payload map[string]any) map[string]string {
|
||||
t.Helper()
|
||||
|
||||
reviewers := nestedArray(t, payload, "data", "reviewers")
|
||||
statuses := make(map[string]string, len(reviewers))
|
||||
for _, item := range reviewers {
|
||||
reviewer, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected reviewer object, got %#v", item)
|
||||
}
|
||||
role, _ := reviewer["reviewer_role"].(string)
|
||||
status, _ := reviewer["status"].(string)
|
||||
statuses[role] = status
|
||||
}
|
||||
return statuses
|
||||
}
|
||||
Reference in New Issue
Block a user