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