Add orch control commands

This commit is contained in:
2026-03-19 14:21:20 +08:00
parent f1785b314f
commit ae272855f6
10 changed files with 1644 additions and 43 deletions
+421
View File
@@ -879,3 +879,424 @@ func TestOrchWaitTimesOutWithoutMatchingEvent(t *testing.T) {
t.Fatalf("expected next_event_id 0 on timeout, got %#v", nextEventID)
}
}
func TestOrchRetryCreatesNewAttempt(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
repoPath := initGitRepo(t)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_retry_001",
"--goal", "Validate retry behavior",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_retry_001",
"--task", "T1",
"--title", "Implement backend",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_retry_001",
"--task", "T1",
"--repo-path", repoPath,
"--workspace-root", ".orch/worktrees",
"--strict-worktree",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
firstWorktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"fail",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Build failed",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_retry_001",
)
retryOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"retry",
"--run", "run_blog_retry_001",
"--task", "T1",
"--body", "Retry after fixing the failure.",
)
var retryResp map[string]any
mustDecodeJSON(t, retryOut, &retryResp)
if got := nestedString(t, retryResp, "data", "task", "status"); got != "dispatched" {
t.Fatalf("expected retried task to be dispatched, got %q", got)
}
if got := nestedValue(t, retryResp, "data", "attempt", "attempt_no").(float64); got != 2 {
t.Fatalf("expected retry attempt 2, got %#v", got)
}
secondThreadID := nestedString(t, retryResp, "data", "attempt", "thread_id")
if secondThreadID == threadID {
t.Fatalf("expected retry to create a new thread, got same thread %q", secondThreadID)
}
secondWorktreePath := nestedString(t, retryResp, "data", "attempt", "worktree_path")
if secondWorktreePath == firstWorktreePath {
t.Fatalf("expected retry to create a new worktree, got reused path %q", secondWorktreePath)
}
if _, err := os.Stat(secondWorktreePath); err != nil {
t.Fatalf("stat retry worktree %s: %v", secondWorktreePath, err)
}
}
func TestOrchReassignCancelsOldThreadAndDispatchesNewAttempt(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
repoPath := initGitRepo(t)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_reassign_001",
"--goal", "Validate reassign behavior",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_reassign_001",
"--task", "T1",
"--title", "Implement backend",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_reassign_001",
"--task", "T1",
"--repo-path", repoPath,
"--workspace-root", ".orch/worktrees",
"--strict-worktree",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
originalThreadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", originalThreadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"update",
"--agent", "worker-a",
"--thread", originalThreadID,
"--status", "blocked",
"--summary", "Need product decision",
"--payload-json", `{"question":"Proceed with v1 scope?"}`,
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_reassign_001",
)
reassignOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"reassign",
"--run", "run_blog_reassign_001",
"--task", "T1",
"--to", "worker-b",
"--reason", "Try another worker with clearer ownership.",
)
var reassignResp map[string]any
mustDecodeJSON(t, reassignOut, &reassignResp)
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 reassignment to create a new thread, got %q", newThreadID)
}
showOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"show",
"--thread", originalThreadID,
)
var showResp map[string]any
mustDecodeJSON(t, showOut, &showResp)
if got := nestedString(t, showResp, "data", "thread", "status"); got != "cancelled" {
t.Fatalf("expected old reassigned thread to be cancelled, got %q", got)
}
}
func TestOrchCancelTaskAndRun(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_cancel_001",
"--goal", "Validate cancel behavior",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_cancel_001",
"--task", "T1",
"--title", "Implement backend",
"--default-to", "worker-a",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_cancel_001",
"--task", "T2",
"--title", "Implement frontend",
"--default-to", "worker-b",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_cancel_001",
"--task", "T1",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
runOrchCommand(
t,
"--db", dbPath,
"--json",
"cancel",
"--run", "run_blog_cancel_001",
"--task", "T1",
"--reason", "Task is no longer needed.",
)
statusOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"status",
"--run", "run_blog_cancel_001",
)
var statusResp map[string]any
mustDecodeJSON(t, statusOut, &statusResp)
tasks := nestedArray(t, statusResp, "data", "tasks")
taskStatuses := map[string]string{}
for _, item := range tasks {
task, ok := item.(map[string]any)
if !ok {
t.Fatalf("expected task object, got %#v", item)
}
taskStatuses[task["task_id"].(string)] = task["status"].(string)
}
if taskStatuses["T1"] != "cancelled" {
t.Fatalf("expected T1 cancelled, got %q", taskStatuses["T1"])
}
if taskStatuses["T2"] == "cancelled" {
t.Fatalf("expected T2 to remain active before run cancel, got %q", taskStatuses["T2"])
}
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 task thread to be cancelled, got %q", got)
}
runOrchCommand(
t,
"--db", dbPath,
"--json",
"cancel",
"--run", "run_blog_cancel_001",
"--reason", "Stop the run.",
)
statusOut = runOrchCommand(
t,
"--db", dbPath,
"--json",
"status",
"--run", "run_blog_cancel_001",
)
mustDecodeJSON(t, statusOut, &statusResp)
if got := nestedString(t, statusResp, "data", "run", "status"); got != "cancelled" {
t.Fatalf("expected cancelled run, got %q", got)
}
tasks = nestedArray(t, statusResp, "data", "tasks")
for _, item := range tasks {
task, ok := item.(map[string]any)
if !ok {
t.Fatalf("expected task object, got %#v", item)
}
if got, _ := task["status"].(string); got != "cancelled" {
t.Fatalf("expected all tasks cancelled after run cancel, got %#v", task["status"])
}
}
}
func TestOrchCleanupRemovesCompletedWorktree(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
repoPath := initGitRepo(t)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"run", "init",
"--run", "run_blog_cleanup_001",
"--goal", "Validate cleanup behavior",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"task", "add",
"--run", "run_blog_cleanup_001",
"--task", "T1",
"--title", "Implement backend",
"--default-to", "worker-a",
)
dispatchOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"dispatch",
"--run", "run_blog_cleanup_001",
"--task", "T1",
"--repo-path", repoPath,
"--workspace-root", ".orch/worktrees",
"--strict-worktree",
)
var dispatchResp map[string]any
mustDecodeJSON(t, dispatchOut, &dispatchResp)
threadID := nestedString(t, dispatchResp, "data", "attempt", "thread_id")
worktreePath := nestedString(t, dispatchResp, "data", "attempt", "worktree_path")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-a",
"--thread", threadID,
)
runInboxCommand(
t,
"--db", dbPath,
"--json",
"done",
"--agent", "worker-a",
"--thread", threadID,
"--summary", "Backend complete",
)
runOrchCommand(
t,
"--db", dbPath,
"--json",
"reconcile",
"--run", "run_blog_cleanup_001",
)
cleanupOut := runOrchCommand(
t,
"--db", dbPath,
"--json",
"cleanup",
"--run", "run_blog_cleanup_001",
"--task", "T1",
)
var cleanupResp map[string]any
mustDecodeJSON(t, cleanupOut, &cleanupResp)
cleaned := nestedArray(t, cleanupResp, "data", "cleaned")
if len(cleaned) != 1 {
t.Fatalf("expected one cleaned attempt, got %#v", cleaned)
}
if _, err := os.Stat(worktreePath); !os.IsNotExist(err) {
t.Fatalf("expected cleaned worktree path to be removed, err=%v", err)
}
}