Add inbox and orch command contract tests
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchCleanupAllCompletedRemovesMultipleWorktrees verifies cleanup removes every completed worktree when --all-completed is used.
|
||||
func TestOrchCleanupAllCompletedRemovesMultipleWorktrees(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
repoPath := initGitRepo(t)
|
||||
runID := "run_blog_cleanup_all_completed_001"
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Validate cleanup across multiple completed worktrees",
|
||||
)
|
||||
|
||||
worktreeOne := seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, runID, "T1", "worker-a")
|
||||
worktreeTwo := seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, runID, "T2", "worker-b")
|
||||
|
||||
cleanupOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cleanup",
|
||||
"--run", runID,
|
||||
"--all-completed",
|
||||
)
|
||||
|
||||
var cleanupResp map[string]any
|
||||
mustDecodeJSON(t, cleanupOut, &cleanupResp)
|
||||
cleaned := nestedArray(t, cleanupResp, "data", "cleaned")
|
||||
if len(cleaned) != 2 {
|
||||
t.Fatalf("expected two cleaned attempts, got %#v", cleaned)
|
||||
}
|
||||
|
||||
for _, worktreePath := range []string{worktreeOne, worktreeTwo} {
|
||||
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected cleaned worktree path %q to be removed, err=%v", worktreePath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCleanupForceRemovesCompletedWorktrees verifies cleanup accepts --force while removing completed worktrees selected by --all-completed.
|
||||
func TestOrchCleanupForceRemovesCompletedWorktrees(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
repoPath := initGitRepo(t)
|
||||
runID := "run_blog_cleanup_force_001"
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Validate cleanup force flag acceptance",
|
||||
)
|
||||
|
||||
worktreePath := seedCompletedCodeTaskForCleanupCouncilTests(t, dbPath, repoPath, runID, "T1", "worker-a")
|
||||
|
||||
cleanupOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"cleanup",
|
||||
"--run", runID,
|
||||
"--all-completed",
|
||||
"--force",
|
||||
)
|
||||
|
||||
var cleanupResp map[string]any
|
||||
mustDecodeJSON(t, cleanupOut, &cleanupResp)
|
||||
cleaned := nestedArray(t, cleanupResp, "data", "cleaned")
|
||||
if len(cleaned) != 1 {
|
||||
t.Fatalf("expected one cleaned attempt with --force, got %#v", cleaned)
|
||||
}
|
||||
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected forced cleanup worktree path %q to be removed, err=%v", worktreePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchCouncilStartPersistsCustomInputs verifies council start stores explicit target-file, repo-path, task-id, review mode, and only-unanimous settings.
|
||||
func TestOrchCouncilStartPersistsCustomInputs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
repoPath := initGitRepo(t)
|
||||
targetFile := filepath.Join(tempDir, "target.md")
|
||||
if err := os.WriteFile(targetFile, []byte("# Review target\n\nInspect the API changes.\n"), 0o644); err != nil {
|
||||
t.Fatalf("write council target file: %v", err)
|
||||
}
|
||||
|
||||
startOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"council", "start",
|
||||
"--run", "council_blog_custom_inputs_001",
|
||||
"--target-file", targetFile,
|
||||
"--repo-path", repoPath,
|
||||
"--task-id", "TASK-42",
|
||||
"--target-type", "repo",
|
||||
"--mode", "review",
|
||||
"--output", "json",
|
||||
"--only-unanimous",
|
||||
)
|
||||
|
||||
var startResp map[string]any
|
||||
mustDecodeJSON(t, startOut, &startResp)
|
||||
if got := nestedString(t, startResp, "data", "run_id"); got != "council_blog_custom_inputs_001" {
|
||||
t.Fatalf("expected council run id, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, startResp, "data", "mode"); got != "review" {
|
||||
t.Fatalf("expected council mode review, got %q", got)
|
||||
}
|
||||
reviewers := nestedArray(t, startResp, "data", "reviewers")
|
||||
if len(reviewers) != 3 {
|
||||
t.Fatalf("expected three reviewers, got %#v", reviewers)
|
||||
}
|
||||
|
||||
sqlDB, err := openOrchDB(t.Context(), dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open orch db: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
|
||||
var (
|
||||
mode string
|
||||
targetType string
|
||||
outputMode string
|
||||
onlyUnanimous int
|
||||
)
|
||||
if err := sqlDB.QueryRowContext(
|
||||
t.Context(),
|
||||
`SELECT mode, target_type, output_mode, only_unanimous
|
||||
FROM council_runs
|
||||
WHERE run_id = ?`,
|
||||
"council_blog_custom_inputs_001",
|
||||
).Scan(&mode, &targetType, &outputMode, &onlyUnanimous); err != nil {
|
||||
t.Fatalf("query council_runs: %v", err)
|
||||
}
|
||||
if mode != "review" || targetType != "repo" || outputMode != "json" || onlyUnanimous != 1 {
|
||||
t.Fatalf("unexpected council run metadata: mode=%q targetType=%q outputMode=%q onlyUnanimous=%d", mode, targetType, outputMode, onlyUnanimous)
|
||||
}
|
||||
|
||||
var (
|
||||
storedTargetFile string
|
||||
storedRepoPath string
|
||||
storedTaskID string
|
||||
)
|
||||
if err := sqlDB.QueryRowContext(
|
||||
t.Context(),
|
||||
`SELECT target_file, repo_path, target_task_id
|
||||
FROM council_inputs
|
||||
WHERE run_id = ?`,
|
||||
"council_blog_custom_inputs_001",
|
||||
).Scan(&storedTargetFile, &storedRepoPath, &storedTaskID); err != nil {
|
||||
t.Fatalf("query council_inputs: %v", err)
|
||||
}
|
||||
if storedTargetFile != targetFile || storedRepoPath != repoPath || storedTaskID != "TASK-42" {
|
||||
t.Fatalf("unexpected council inputs: targetFile=%q repoPath=%q taskID=%q", storedTargetFile, storedRepoPath, storedTaskID)
|
||||
}
|
||||
}
|
||||
|
||||
func seedCompletedCodeTaskForCleanupCouncilTests(t *testing.T, dbPath, repoPath, runID, taskID, agent string) string {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", "Complete cleanup target "+taskID,
|
||||
"--default-to", agent,
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--execution-mode", "code",
|
||||
"--repo-path", repoPath,
|
||||
"--workspace-root", ".orch/worktrees",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
|
||||
worktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", agent,
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"done",
|
||||
"--agent", agent,
|
||||
"--thread", threadID,
|
||||
"--summary", "Completed cleanup target "+taskID,
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
|
||||
return worktreePath
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchTaskAddRejectsInvalidMetadataJSON verifies orch task add rejects invalid metadata JSON.
|
||||
func TestOrchTaskAddRejectsInvalidMetadataJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_008",
|
||||
"--goal", "Validate task metadata input",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_008",
|
||||
"--task", "T1",
|
||||
"--title", "Implement retry policy",
|
||||
"--metadata-json", `{"repo":`,
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_input exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
}
|
||||
|
||||
// TestOrchDepAddCreatesDependencyAndAffectsReadyState verifies orch dep add returns the dependency and blocks the dependent task from ready output.
|
||||
func TestOrchDepAddCreatesDependencyAndAffectsReadyState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_dep_001",
|
||||
"--goal", "Validate dependency contracts",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_dep_001",
|
||||
"--task", "T1",
|
||||
"--title", "Prerequisite task",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_dep_001",
|
||||
"--task", "T2",
|
||||
"--title", "Dependent task",
|
||||
"--default-to", "worker-b",
|
||||
)
|
||||
|
||||
depOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dep", "add",
|
||||
"--run", "run_blog_dep_001",
|
||||
"--task", "T2",
|
||||
"--depends-on", "T1",
|
||||
)
|
||||
|
||||
var depResp map[string]any
|
||||
mustDecodeJSON(t, depOut, &depResp)
|
||||
if got := nestedString(t, depResp, "data", "dependency", "run_id"); got != "run_blog_dep_001" {
|
||||
t.Fatalf("expected dependency run_id run_blog_dep_001, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, depResp, "data", "dependency", "task_id"); got != "T2" {
|
||||
t.Fatalf("expected dependency task_id T2, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, depResp, "data", "dependency", "depends_on_task_id"); got != "T1" {
|
||||
t.Fatalf("expected dependency depends_on_task_id T1, got %q", got)
|
||||
}
|
||||
|
||||
readyOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"ready",
|
||||
"--run", "run_blog_dep_001",
|
||||
)
|
||||
|
||||
var readyResp map[string]any
|
||||
mustDecodeJSON(t, readyOut, &readyResp)
|
||||
readyTasks := nestedArray(t, readyResp, "data", "tasks")
|
||||
if len(readyTasks) != 1 {
|
||||
t.Fatalf("expected one ready task after dependency add, got %#v", readyTasks)
|
||||
}
|
||||
readyTask, ok := readyTasks[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected ready task object, got %#v", readyTasks[0])
|
||||
}
|
||||
if got, _ := readyTask["task_id"].(string); got != "T1" {
|
||||
t.Fatalf("expected only prerequisite task T1 to remain ready, got %#v", readyTask["task_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchDepAddRejectsMissingDependencyTask verifies orch dep add rejects a dependency on a missing task.
|
||||
func TestOrchDepAddRejectsMissingDependencyTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_dep_002",
|
||||
"--goal", "Validate dependency not found",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_dep_002",
|
||||
"--task", "T2",
|
||||
"--title", "Dependent task",
|
||||
"--default-to", "worker-b",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dep", "add",
|
||||
"--run", "run_blog_dep_002",
|
||||
"--task", "T2",
|
||||
"--depends-on", "T9",
|
||||
)
|
||||
if exitCode != 40 {
|
||||
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "not_found")
|
||||
}
|
||||
|
||||
// TestOrchDepAddRejectsMissingTask verifies orch dep add rejects a missing source task.
|
||||
func TestOrchDepAddRejectsMissingTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_blog_dep_003",
|
||||
"--goal", "Validate source task not found",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_blog_dep_003",
|
||||
"--task", "T1",
|
||||
"--title", "Prerequisite task",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dep", "add",
|
||||
"--run", "run_blog_dep_003",
|
||||
"--task", "T9",
|
||||
"--depends-on", "T1",
|
||||
)
|
||||
if exitCode != 40 {
|
||||
t.Fatalf("expected not_found exit code 40, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "not_found")
|
||||
}
|
||||
|
||||
// TestOrchVerifyStatusReturnsTaskAttemptSpecAndGate verifies orch verify status returns task, attempt, spec, and gate details.
|
||||
func TestOrchVerifyStatusReturnsTaskAttemptSpecAndGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath, specFile := seedVerifyStatusTaskForCoreContracts(t, "run_verify_core_001", true)
|
||||
|
||||
verifyStatusOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "status",
|
||||
"--run", "run_verify_core_001",
|
||||
"--task", "T1",
|
||||
)
|
||||
|
||||
var verifyStatusResp map[string]any
|
||||
mustDecodeJSON(t, verifyStatusOut, &verifyStatusResp)
|
||||
if got := nestedString(t, verifyStatusResp, "data", "task", "task_id"); got != "T1" {
|
||||
t.Fatalf("expected task_id T1, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyStatusResp, "data", "task", "status"); got != "verifying" {
|
||||
t.Fatalf("expected task status verifying, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyStatusResp, "data", "attempt", "assigned_to"); got != "worker-a" {
|
||||
t.Fatalf("expected attempt assigned_to worker-a, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyStatusResp, "data", "spec", "spec_file"); got != specFile {
|
||||
t.Fatalf("expected spec_file %q, got %q", specFile, got)
|
||||
}
|
||||
requiredChecks := nestedArray(t, verifyStatusResp, "data", "gate", "required_checks")
|
||||
if len(requiredChecks) != 1 || requiredChecks[0] != "lint" {
|
||||
t.Fatalf("expected gate required_checks [lint], got %#v", requiredChecks)
|
||||
}
|
||||
pendingChecks := nestedArray(t, verifyStatusResp, "data", "gate", "pending_checks")
|
||||
if len(pendingChecks) != 1 || pendingChecks[0] != "lint" {
|
||||
t.Fatalf("expected gate pending_checks [lint], got %#v", pendingChecks)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyStatusRejectsMissingExplicitAttempt verifies orch verify status rejects an explicit attempt that does not exist.
|
||||
func TestOrchVerifyStatusRejectsMissingExplicitAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath, _ := seedVerifyStatusTaskForCoreContracts(t, "run_verify_core_002", false)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "status",
|
||||
"--run", "run_verify_core_002",
|
||||
"--task", "T1",
|
||||
"--attempt", "2",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_state")
|
||||
}
|
||||
|
||||
func seedVerifyStatusTaskForCoreContracts(t *testing.T, runID string, moveToVerifying bool) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
specFile := filepath.Join(tempDir, "task.md")
|
||||
if err := os.WriteFile(specFile, []byte("# Task\n\nShip the verifier-backed component change.\n"), 0o644); err != nil {
|
||||
t.Fatalf("write spec file: %v", err)
|
||||
}
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Seed verify status contract task",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--title", "Implement verifier-backed task",
|
||||
"--default-to", "worker-a",
|
||||
"--spec-file", specFile,
|
||||
"--required-check", "lint",
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
"--body", "Implement the gated task.",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
|
||||
|
||||
if moveToVerifying {
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"done",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
"--summary", "Implementation finished",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
}
|
||||
|
||||
return dbPath, specFile
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchAnswerPropagatesBodyFileContent verifies orch answer reads the leader response body from a file.
|
||||
func TestOrchAnswerPropagatesBodyFileContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
bodyPath := filepath.Join(tempDir, "answer.md")
|
||||
body := "Use stdout for the MVP logs.\nKeep stderr for unexpected failures.\n"
|
||||
if err := os.WriteFile(bodyPath, []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("write answer body file: %v", err)
|
||||
}
|
||||
|
||||
threadID := seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_file_001", "T2", "worker-b")
|
||||
|
||||
answerOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"answer",
|
||||
"--run", "run_blog_answer_file_001",
|
||||
"--task", "T2",
|
||||
"--body-file", bodyPath,
|
||||
)
|
||||
|
||||
var answerResp map[string]any
|
||||
mustDecodeJSON(t, answerOut, &answerResp)
|
||||
if got := nestedString(t, answerResp, "data", "message", "kind"); got != "answer" {
|
||||
t.Fatalf("expected answer message kind, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, answerResp, "data", "message", "body"); got != body {
|
||||
t.Fatalf("expected answer body %q, got %q", body, got)
|
||||
}
|
||||
|
||||
showOut := runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"show",
|
||||
"--thread", threadID,
|
||||
)
|
||||
|
||||
var showResp map[string]any
|
||||
mustDecodeJSON(t, showOut, &showResp)
|
||||
messages := nestedArray(t, showResp, "data", "messages")
|
||||
lastMessage, ok := messages[len(messages)-1].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected last message object, got %#v", messages[len(messages)-1])
|
||||
}
|
||||
if got, _ := lastMessage["body"].(string); got != body {
|
||||
t.Fatalf("expected latest inbox message body %q, got %#v", body, lastMessage["body"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchAnswerRejectsWithoutActiveBlockedQuestion verifies orch answer rejects tasks without an active blocked question.
|
||||
func TestOrchAnswerRejectsWithoutActiveBlockedQuestion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedDispatchedTaskForAnswerEdgeTests(t, dbPath, "run_blog_answer_ready_001", "T2", "worker-b")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"answer",
|
||||
"--run", "run_blog_answer_ready_001",
|
||||
"--task", "T2",
|
||||
"--body", "Use stdout for the MVP logs.",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_state")
|
||||
assertErrorMessageContains(t, stdout, "blocked")
|
||||
}
|
||||
|
||||
// TestOrchBlockedHelpMentionsCompactQueue verifies blocked help explains the compact queue leaders inspect before answering.
|
||||
func TestOrchBlockedHelpMentionsCompactQueue(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stdout, stderr, exitCode := executeOrchCommand("blocked", "--help")
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
|
||||
combined := stdout + stderr
|
||||
if !strings.Contains(combined, "latest question the leader needs to answer") {
|
||||
t.Fatalf("expected blocked help to explain latest question role, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "Use blocked before answer") {
|
||||
t.Fatalf("expected blocked help to explain inspect-before-answer flow, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "compact queue of unresolved worker questions") {
|
||||
t.Fatalf("expected blocked help to explain compact queue role, got:\n%s", combined)
|
||||
}
|
||||
}
|
||||
|
||||
func seedDispatchedTaskForAnswerEdgeTests(t *testing.T, dbPath, runID, taskID, agent string) {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Prepare dispatched task for answer edge tests",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", "Build frontend",
|
||||
"--default-to", agent,
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--execution-mode", "analysis",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchDispatchReadsBodyFromBodyFileAndCarriesSpecPayload verifies dispatch loads body-file input and includes spec and verification policy in the task payload.
|
||||
func TestOrchDispatchReadsBodyFromBodyFileAndCarriesSpecPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
specFile := filepath.Join(tempDir, "task.md")
|
||||
bodyFile := filepath.Join(tempDir, "dispatch-body.txt")
|
||||
specBody := "# Task\n\nImplement the spec-aware worker handoff.\n"
|
||||
dispatchBody := "Worker brief loaded from file.\n"
|
||||
if err := os.WriteFile(specFile, []byte(specBody), 0o644); err != nil {
|
||||
t.Fatalf("write spec file: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(bodyFile, []byte(dispatchBody), 0o644); err != nil {
|
||||
t.Fatalf("write body file: %v", err)
|
||||
}
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_dispatch_contracts_001",
|
||||
"--goal", "Validate dispatch payload contracts",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_dispatch_contracts_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement worker handoff",
|
||||
"--summary", "Ship the spec-aware task payload",
|
||||
"--default-to", "worker-a",
|
||||
"--spec-file", specFile,
|
||||
"--check-profile", "cadence_component",
|
||||
"--required-check", "lint",
|
||||
"--allowed-path", "packages/orch-runtime",
|
||||
"--blocked-path", "scripts/release.sh",
|
||||
"--metadata-json", `{"team":"core"}`,
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_dispatch_contracts_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
"--to", "worker-a",
|
||||
"--body-file", bodyFile,
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
if got := nestedString(t, dispatchResp, "data", "message", "body"); got != dispatchBody {
|
||||
t.Fatalf("expected dispatch body %q, got %q", dispatchBody, got)
|
||||
}
|
||||
if got := nestedString(t, dispatchResp, "data", "task", "status"); got != "dispatched" {
|
||||
t.Fatalf("expected task status dispatched, got %q", got)
|
||||
}
|
||||
|
||||
payload, ok := nestedValue(t, dispatchResp, "data", "message", "payload_json").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, dispatchResp, "data", "message", "payload_json"))
|
||||
}
|
||||
if got, _ := payload["execution_mode"].(string); got != "analysis" {
|
||||
t.Fatalf("expected execution_mode analysis, got %#v", payload["execution_mode"])
|
||||
}
|
||||
|
||||
spec, ok := payload["spec"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected payload spec object, got %#v", payload["spec"])
|
||||
}
|
||||
if got, _ := spec["file"].(string); got != specFile {
|
||||
t.Fatalf("expected spec file %q, got %#v", specFile, spec["file"])
|
||||
}
|
||||
if got, _ := spec["body"].(string); got != specBody {
|
||||
t.Fatalf("expected spec body %q, got %#v", specBody, spec["body"])
|
||||
}
|
||||
if got, _ := spec["check_profile"].(string); got != "cadence_component" {
|
||||
t.Fatalf("expected check_profile cadence_component, got %#v", spec["check_profile"])
|
||||
}
|
||||
requiredChecks, ok := spec["required_checks"].([]any)
|
||||
if !ok || len(requiredChecks) != 1 || requiredChecks[0] != "lint" {
|
||||
t.Fatalf("expected required_checks [lint], got %#v", spec["required_checks"])
|
||||
}
|
||||
allowedPaths, ok := spec["allowed_paths"].([]any)
|
||||
if !ok || len(allowedPaths) != 1 || allowedPaths[0] != "packages/orch-runtime" {
|
||||
t.Fatalf("expected allowed_paths [packages/orch-runtime], got %#v", spec["allowed_paths"])
|
||||
}
|
||||
blockedPaths, ok := spec["blocked_paths"].([]any)
|
||||
if !ok || len(blockedPaths) != 1 || blockedPaths[0] != "scripts/release.sh" {
|
||||
t.Fatalf("expected blocked_paths [scripts/release.sh], got %#v", spec["blocked_paths"])
|
||||
}
|
||||
metadata, ok := spec["metadata"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected payload spec metadata object, got %#v", spec["metadata"])
|
||||
}
|
||||
if got, _ := metadata["team"].(string); got != "core" {
|
||||
t.Fatalf("expected payload spec metadata.team core, got %#v", metadata["team"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordMovesTaskToFailedOnFailedCheck verifies verify record moves a verifying task to failed when a required check fails.
|
||||
func TestOrchVerifyRecordMovesTaskToFailedOnFailedCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_contracts_001", "T1", []string{"lint"})
|
||||
|
||||
verifyOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_contracts_001",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "failed",
|
||||
"--summary", "lint failed",
|
||||
)
|
||||
|
||||
var verifyResp map[string]any
|
||||
mustDecodeJSON(t, verifyOut, &verifyResp)
|
||||
if got := nestedString(t, verifyResp, "data", "task", "status"); got != "failed" {
|
||||
t.Fatalf("expected task status failed, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "gate", "status"); got != "failed" {
|
||||
t.Fatalf("expected gate status failed, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "check", "status"); got != "failed" {
|
||||
t.Fatalf("expected recorded check status failed, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordUpdatesExistingNamedCheck verifies verify record updates an existing named check result and recomputes the gate.
|
||||
func TestOrchVerifyRecordUpdatesExistingNamedCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_contracts_002", "T1", []string{"lint", "test"})
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_contracts_002",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "failed",
|
||||
"--summary", "first lint failure",
|
||||
"--recorded-by", "qa-1",
|
||||
)
|
||||
|
||||
verifyOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_contracts_002",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "passed",
|
||||
"--summary", "lint fixed",
|
||||
"--recorded-by", "qa-2",
|
||||
)
|
||||
|
||||
var verifyResp map[string]any
|
||||
mustDecodeJSON(t, verifyOut, &verifyResp)
|
||||
if got := nestedString(t, verifyResp, "data", "task", "status"); got != "verifying" {
|
||||
t.Fatalf("expected task status verifying after updating lint result, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "gate", "status"); got != "pending" {
|
||||
t.Fatalf("expected gate status pending after updating lint result, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "check", "status"); got != "passed" {
|
||||
t.Fatalf("expected updated check status passed, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "check", "summary"); got != "lint fixed" {
|
||||
t.Fatalf("expected updated check summary lint fixed, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "check", "recorded_by"); got != "qa-2" {
|
||||
t.Fatalf("expected updated check recorded_by qa-2, got %q", got)
|
||||
}
|
||||
|
||||
verifyStatusOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "status",
|
||||
"--run", "run_verify_contracts_002",
|
||||
"--task", "T1",
|
||||
)
|
||||
|
||||
var verifyStatusResp map[string]any
|
||||
mustDecodeJSON(t, verifyStatusOut, &verifyStatusResp)
|
||||
pendingChecks := nestedArray(t, verifyStatusResp, "data", "gate", "pending_checks")
|
||||
if len(pendingChecks) != 1 || pendingChecks[0] != "test" {
|
||||
t.Fatalf("expected only test to remain pending after lint update, got %#v", pendingChecks)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchVerifyRecordRoundTripsBodyMetadataAndRecorder verifies verify record preserves body-file content, metadata JSON, and recorder identity in the returned check result.
|
||||
func TestOrchVerifyRecordRoundTripsBodyMetadataAndRecorder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
bodyFile := filepath.Join(tempDir, "verify.txt")
|
||||
body := "verification details from file\n"
|
||||
if err := os.WriteFile(bodyFile, []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("write verify body file: %v", err)
|
||||
}
|
||||
|
||||
seedVerifyingTaskForVerifyAndRecoveryTests(t, dbPath, "run_verify_contracts_003", "T1", []string{"lint"})
|
||||
|
||||
verifyOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", "run_verify_contracts_003",
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "passed",
|
||||
"--summary", "lint clean",
|
||||
"--body-file", bodyFile,
|
||||
"--metadata-json", `{"ticket":"QA-1"}`,
|
||||
"--recorded-by", "qa-bot",
|
||||
)
|
||||
|
||||
var verifyResp map[string]any
|
||||
mustDecodeJSON(t, verifyOut, &verifyResp)
|
||||
if got := nestedString(t, verifyResp, "data", "task", "status"); got != "done" {
|
||||
t.Fatalf("expected task status done after the only required check passes, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "check", "body"); got != body {
|
||||
t.Fatalf("expected check body %q, got %q", body, got)
|
||||
}
|
||||
if got := nestedString(t, verifyResp, "data", "check", "recorded_by"); got != "qa-bot" {
|
||||
t.Fatalf("expected check recorded_by qa-bot, got %q", got)
|
||||
}
|
||||
metadata, ok := nestedValue(t, verifyResp, "data", "check", "metadata_json").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected check metadata_json object, got %#v", nestedValue(t, verifyResp, "data", "check", "metadata_json"))
|
||||
}
|
||||
if got, _ := metadata["ticket"].(string); got != "QA-1" {
|
||||
t.Fatalf("expected metadata_json.ticket QA-1, got %#v", metadata["ticket"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchRetryRejectsNonFailedTask verifies retry rejects tasks that are not currently failed.
|
||||
func TestOrchRetryRejectsNonFailedTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", "run_retry_contracts_001",
|
||||
"--goal", "Validate retry state guards",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", "run_retry_contracts_001",
|
||||
"--task", "T1",
|
||||
"--title", "Implement retry guard",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", "run_retry_contracts_001",
|
||||
"--task", "T1",
|
||||
"--execution-mode", "analysis",
|
||||
"--to", "worker-a",
|
||||
)
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", "run_retry_contracts_001",
|
||||
"--task", "T1",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected invalid_state exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_state")
|
||||
}
|
||||
|
||||
// TestOrchRetryUsesBodyFileForReplacementAttempt verifies retry loads the replacement worker brief from body-file.
|
||||
func TestOrchRetryUsesBodyFileForReplacementAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "coord.db")
|
||||
bodyFile := filepath.Join(tempDir, "retry-body.txt")
|
||||
body := "Retry after the failure with the updated brief.\n"
|
||||
if err := os.WriteFile(bodyFile, []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("write retry body file: %v", err)
|
||||
}
|
||||
|
||||
originalThreadID := seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_retry_contracts_002", "T1")
|
||||
|
||||
retryOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", "run_retry_contracts_002",
|
||||
"--task", "T1",
|
||||
"--body-file", bodyFile,
|
||||
)
|
||||
|
||||
var retryResp map[string]any
|
||||
mustDecodeJSON(t, retryOut, &retryResp)
|
||||
if got := nestedString(t, retryResp, "data", "task", "status"); got != "dispatched" {
|
||||
t.Fatalf("expected task status dispatched after retry, got %q", got)
|
||||
}
|
||||
newThreadID := nestedString(t, retryResp, "data", "attempt", "thread_id")
|
||||
if newThreadID == originalThreadID {
|
||||
t.Fatalf("expected retry to create a new thread, got %q", newThreadID)
|
||||
}
|
||||
|
||||
showOut := runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"show",
|
||||
"--thread", newThreadID,
|
||||
)
|
||||
|
||||
var showResp map[string]any
|
||||
mustDecodeJSON(t, showOut, &showResp)
|
||||
lastMessage := lastThreadMessageForVerifyAndRecoveryTests(t, showResp)
|
||||
if got, _ := lastMessage["body"].(string); got != body {
|
||||
t.Fatalf("expected retry message body %q, got %#v", body, lastMessage["body"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrchReassignWorksFromFailedTask verifies reassign can move a failed task to a new worker by creating a replacement attempt.
|
||||
func TestOrchReassignWorksFromFailedTask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
originalThreadID := seedFailedTaskForVerifyAndRecoveryTests(t, dbPath, "run_reassign_contracts_001", "T1")
|
||||
|
||||
reassignOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reassign",
|
||||
"--run", "run_reassign_contracts_001",
|
||||
"--task", "T1",
|
||||
"--to", "worker-b",
|
||||
"--reason", "Route the failed task to a different worker.",
|
||||
)
|
||||
|
||||
var reassignResp map[string]any
|
||||
mustDecodeJSON(t, reassignOut, &reassignResp)
|
||||
if got := nestedString(t, reassignResp, "data", "task", "status"); got != "dispatched" {
|
||||
t.Fatalf("expected task status dispatched after reassign, got %q", got)
|
||||
}
|
||||
if got := nestedString(t, reassignResp, "data", "attempt", "assigned_to"); got != "worker-b" {
|
||||
t.Fatalf("expected reassigned attempt to target worker-b, got %q", got)
|
||||
}
|
||||
if got := nestedValue(t, reassignResp, "data", "attempt", "attempt_no").(float64); got != 2 {
|
||||
t.Fatalf("expected reassign attempt 2, got %#v", got)
|
||||
}
|
||||
newThreadID := nestedString(t, reassignResp, "data", "attempt", "thread_id")
|
||||
if newThreadID == originalThreadID {
|
||||
t.Fatalf("expected reassign to create a new thread, got %q", newThreadID)
|
||||
}
|
||||
}
|
||||
|
||||
func seedVerifyingTaskForVerifyAndRecoveryTests(t *testing.T, dbPath, runID, taskID string, requiredChecks []string) string {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Prepare verifying task for contract tests",
|
||||
)
|
||||
|
||||
args := []string{
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", "Prepare verifying task",
|
||||
"--default-to", "worker-a",
|
||||
}
|
||||
for _, check := range requiredChecks {
|
||||
args = append(args, "--required-check", check)
|
||||
}
|
||||
runOrchCommand(t, args...)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--execution-mode", "analysis",
|
||||
"--to", "worker-a",
|
||||
"--body", "Prepare the task for verification.",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"done",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
"--summary", "Implementation complete",
|
||||
"--body", "Ready for verification.",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
|
||||
return threadID
|
||||
}
|
||||
|
||||
func seedFailedTaskForVerifyAndRecoveryTests(t *testing.T, dbPath, runID, taskID string) string {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Prepare failed task for recovery tests",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", "Prepare failed task",
|
||||
"--default-to", "worker-a",
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--execution-mode", "analysis",
|
||||
"--to", "worker-a",
|
||||
"--body", "Initial worker brief.",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"fail",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
"--summary", "Worker reported a failure",
|
||||
"--body", "The task failed before completion.",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
|
||||
return threadID
|
||||
}
|
||||
|
||||
func lastThreadMessageForVerifyAndRecoveryTests(t *testing.T, showResp map[string]any) map[string]any {
|
||||
t.Helper()
|
||||
|
||||
messages := nestedArray(t, showResp, "data", "messages")
|
||||
if len(messages) == 0 {
|
||||
t.Fatalf("expected at least one message, got %#v", messages)
|
||||
}
|
||||
lastMessage, ok := messages[len(messages)-1].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected message object, got %#v", messages[len(messages)-1])
|
||||
}
|
||||
return lastMessage
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package orch
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOrchAnswerRejectsInvalidPayloadJSON verifies answer rejects malformed payload JSON.
|
||||
func TestOrchAnswerRejectsInvalidPayloadJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
_ = seedBlockedTaskForAnswerCleanupEdgeTests(t, dbPath, "run_blog_answer_extra_002", "T2", "worker-b")
|
||||
|
||||
stdout, _, exitCode := executeOrchCommand(
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"answer",
|
||||
"--run", "run_blog_answer_extra_002",
|
||||
"--task", "T2",
|
||||
"--payload-json", "not-json",
|
||||
)
|
||||
if exitCode != 30 {
|
||||
t.Fatalf("expected exit code 30, got %d\nstdout:\n%s", exitCode, stdout)
|
||||
}
|
||||
assertErrorJSON(t, stdout, "invalid_input")
|
||||
}
|
||||
|
||||
// TestOrchVerifyStatusSelectsExplicitAttempt verifies verify status can inspect a non-latest attempt by number.
|
||||
func TestOrchVerifyStatusSelectsExplicitAttempt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||
runID := "run_blog_verify_extra_002"
|
||||
seedVerifyingTaskForVerifyContractTests(t, dbPath, runID, "T1")
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "record",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--check", "lint",
|
||||
"--status", "failed",
|
||||
"--summary", "Lint failed",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"retry",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--body", "Retry after fixing lint.",
|
||||
)
|
||||
|
||||
verifyStatusOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"verify", "status",
|
||||
"--run", runID,
|
||||
"--task", "T1",
|
||||
"--attempt", "1",
|
||||
)
|
||||
|
||||
var verifyStatusResp map[string]any
|
||||
mustDecodeJSON(t, verifyStatusOut, &verifyStatusResp)
|
||||
if got := nestedValue(t, verifyStatusResp, "data", "attempt", "attempt_no").(float64); got != 1 {
|
||||
t.Fatalf("expected verify status to select attempt 1, got %#v", got)
|
||||
}
|
||||
if got := nestedString(t, verifyStatusResp, "data", "attempt", "task_id"); got != "T1" {
|
||||
t.Fatalf("expected verify status attempt task T1, got %q", got)
|
||||
}
|
||||
if got := nestedValue(t, verifyStatusResp, "data", "gate", "attempt_no").(float64); got != 1 {
|
||||
t.Fatalf("expected verify gate attempt 1, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func seedVerifyingTaskForVerifyContractTests(t *testing.T, dbPath, runID, taskID string) {
|
||||
t.Helper()
|
||||
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"run", "init",
|
||||
"--run", runID,
|
||||
"--goal", "Prepare verify contract task",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"task", "add",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--title", "Implement gated task",
|
||||
"--default-to", "worker-a",
|
||||
"--required-check", "lint",
|
||||
)
|
||||
|
||||
dispatchOut := runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"dispatch",
|
||||
"--run", runID,
|
||||
"--task", taskID,
|
||||
"--execution-mode", "analysis",
|
||||
"--body", "Implement the gated task.",
|
||||
)
|
||||
|
||||
var dispatchResp map[string]any
|
||||
mustDecodeJSON(t, dispatchOut, &dispatchResp)
|
||||
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
|
||||
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"claim",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
)
|
||||
runInboxCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"done",
|
||||
"--agent", "worker-a",
|
||||
"--thread", threadID,
|
||||
"--summary", "Task complete",
|
||||
)
|
||||
runOrchCommand(
|
||||
t,
|
||||
"--db", dbPath,
|
||||
"--json",
|
||||
"reconcile",
|
||||
"--run", runID,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user