Finalize inbox artifacts and error protocol

This commit is contained in:
2026-03-19 03:25:06 +08:00
parent c3314cd9cf
commit f315d2330d
22 changed files with 659 additions and 86 deletions
+135 -21
View File
@@ -309,12 +309,12 @@ func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
type commandResult struct {
stdout string
stderr string
err error
exit int
}
waitCh := make(chan commandResult, 1)
go func() {
stdout, stderr, err := executeInboxCommand(
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"wait-reply",
@@ -322,7 +322,7 @@ func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
"--after-message", blockedMessageID,
"--timeout-seconds", "2",
)
waitCh <- commandResult{stdout: stdout, stderr: stderr, err: err}
waitCh <- commandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
@@ -346,8 +346,8 @@ func TestInboxRenewWaitReplyAndCancel(t *testing.T) {
t.Fatal("wait-reply command did not return")
}
if waitResult.err != nil {
t.Fatalf("wait-reply failed: %v\nstderr:\n%s", waitResult.err, waitResult.stderr)
if waitResult.exit != 0 {
t.Fatalf("wait-reply failed with exit=%d\nstderr:\n%s\nstdout:\n%s", waitResult.exit, waitResult.stderr, waitResult.stdout)
}
var waitResp map[string]any
@@ -392,12 +392,12 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
type commandResult struct {
stdout string
stderr string
err error
exit int
}
watchCh := make(chan commandResult, 1)
go func() {
stdout, stderr, err := executeInboxCommand(
stdout, stderr, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"watch",
@@ -405,7 +405,7 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
"--status", "pending",
"--timeout-seconds", "2",
)
watchCh <- commandResult{stdout: stdout, stderr: stderr, err: err}
watchCh <- commandResult{stdout: stdout, stderr: stderr, exit: exitCode}
}()
time.Sleep(200 * time.Millisecond)
@@ -420,6 +420,9 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
"--subject", "Build admin editor",
"--summary", "Create the first editor screen",
"--body-file", bodyPath,
"--artifact", bodyPath,
"--artifact-kind", "brief",
"--artifact-metadata-json", `{"label":"task-brief"}`,
"--run", "run_blog_004",
"--task", "T4",
)
@@ -435,8 +438,8 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
t.Fatal("watch command did not return")
}
if watchResult.err != nil {
t.Fatalf("watch failed: %v\nstderr:\n%s", watchResult.err, watchResult.stderr)
if watchResult.exit != 0 {
t.Fatalf("watch failed with exit=%d\nstderr:\n%s\nstdout:\n%s", watchResult.exit, watchResult.stderr, watchResult.stdout)
}
var watchResp map[string]any
@@ -514,29 +517,123 @@ func TestInboxWatchListUnreadAndAppend(t *testing.T) {
if firstMessage["body"] != "Implement the initial admin post editor." {
t.Fatalf("expected body-file content in first message, got %#v", firstMessage["body"])
}
artifacts, ok := firstMessage["artifacts"].([]any)
if !ok || len(artifacts) != 1 {
t.Fatalf("expected one artifact on first message, got %#v", firstMessage["artifacts"])
}
firstArtifact, ok := artifacts[0].(map[string]any)
if !ok {
t.Fatalf("expected artifact object, got %#v", artifacts[0])
}
if firstArtifact["path"] != bodyPath {
t.Fatalf("expected artifact path %q, got %#v", bodyPath, firstArtifact["path"])
}
if firstArtifact["kind"] != "brief" {
t.Fatalf("expected artifact kind brief, got %#v", firstArtifact["kind"])
}
}
func TestInboxJSONErrorsAndExitCodes(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "coord.db")
if _, _, exitCode := executeInboxCommand("--db", dbPath, "--json", "init"); exitCode != 0 {
t.Fatalf("expected init exit code 0, got %d", exitCode)
}
stdout, _, exitCode := executeInboxCommand(
"--db", dbPath,
"--json",
"fetch",
"--agent", "worker-z",
"--status", "pending",
)
if exitCode != 10 {
t.Fatalf("expected fetch no-match exit code 10, got %d", exitCode)
}
assertErrorJSON(t, stdout, "no_matching_work")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", "thr_missing",
)
if exitCode != 40 {
t.Fatalf("expected claim missing-thread exit code 40, got %d", exitCode)
}
assertErrorJSON(t, stdout, "not_found")
sendOut := runInboxCommand(
t,
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Review cache settings",
"--summary", "Check cache config",
)
var sendResp map[string]any
mustDecodeJSON(t, sendOut, &sendResp)
threadID := nestedString(t, sendResp, "data", "thread", "thread_id")
runInboxCommand(
t,
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-z",
"--thread", threadID,
)
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"claim",
"--agent", "worker-y",
"--thread", threadID,
)
if exitCode != 20 {
t.Fatalf("expected lease conflict exit code 20, got %d", exitCode)
}
assertErrorJSON(t, stdout, "lease_conflict")
stdout, _, exitCode = executeInboxCommand(
"--db", dbPath,
"--json",
"send",
"--from", "leader",
"--to", "worker-z",
"--subject", "Invalid body flags",
"--body", "inline",
"--body-file", filepath.Join(t.TempDir(), "missing.md"),
)
if exitCode != 30 {
t.Fatalf("expected invalid input exit code 30, got %d", exitCode)
}
assertErrorJSON(t, stdout, "invalid_input")
}
func runInboxCommand(t *testing.T, args ...string) string {
t.Helper()
stdout, stderr, err := executeInboxCommand(args...)
if err != nil {
t.Fatalf("execute inbox command %v: %v\nstderr:\n%s", args, err, stderr)
stdout, stderr, exitCode := executeInboxCommand(args...)
if exitCode != 0 {
t.Fatalf("execute inbox command %v: exit=%d\nstderr:\n%s\nstdout:\n%s", args, exitCode, stderr, stdout)
}
return stdout
}
func executeInboxCommand(args ...string) (string, string, error) {
cmd := NewRootCmd()
func executeInboxCommand(args ...string) (string, string, int) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stderr)
cmd.SetArgs(args)
err := cmd.Execute()
return stdout.String(), stderr.String(), err
exitCode := Execute(args, &stdout, &stderr)
return stdout.String(), stderr.String(), exitCode
}
func mustDecodeJSON(t *testing.T, raw string, target any) {
@@ -574,3 +671,20 @@ func nestedValue(t *testing.T, value map[string]any, keys ...string) any {
}
return current
}
func assertErrorJSON(t *testing.T, raw string, expectedCode string) {
t.Helper()
var payload map[string]any
mustDecodeJSON(t, raw, &payload)
if ok, _ := payload["ok"].(bool); ok {
t.Fatalf("expected ok=false error payload, got %#v", payload)
}
errorValue, ok := payload["error"].(map[string]any)
if !ok {
t.Fatalf("expected error object, got %#v", payload["error"])
}
if code, _ := errorValue["code"].(string); code != expectedCode {
t.Fatalf("expected error code %q, got %#v", expectedCode, errorValue["code"])
}
}