227 lines
6.3 KiB
Go
227 lines
6.3 KiB
Go
package inbox
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func seedThreadForInboxTests(t *testing.T, dbPath, from, to string) string {
|
|
t.Helper()
|
|
|
|
runInboxCommand(t, "--db", dbPath, "--json", "init")
|
|
|
|
sendOut := runInboxCommand(
|
|
t,
|
|
"--db", dbPath,
|
|
"--json",
|
|
"send",
|
|
"--from", from,
|
|
"--to", to,
|
|
"--subject", "Implement feature X",
|
|
"--summary", "Add retry policy",
|
|
)
|
|
|
|
var sendResp map[string]any
|
|
mustDecodeJSON(t, sendOut, &sendResp)
|
|
return nestedString(t, sendResp, "data", "thread", "thread_id")
|
|
}
|
|
|
|
func seedClaimedThreadForInboxTests(t *testing.T, dbPath, from, to, claimer string) string {
|
|
t.Helper()
|
|
|
|
threadID := seedThreadForInboxTests(t, dbPath, from, to)
|
|
runInboxCommand(
|
|
t,
|
|
"--db", dbPath,
|
|
"--json",
|
|
"claim",
|
|
"--agent", claimer,
|
|
"--thread", threadID,
|
|
"--lease-seconds", "300",
|
|
)
|
|
return threadID
|
|
}
|
|
|
|
func lastThreadMessageFromShow(t *testing.T, showResp map[string]any) map[string]any {
|
|
t.Helper()
|
|
|
|
messagesValue := nestedValue(t, showResp, "data", "messages")
|
|
messages, ok := messagesValue.([]any)
|
|
if !ok || len(messages) == 0 {
|
|
t.Fatalf("expected non-empty messages, got %#v", messagesValue)
|
|
}
|
|
|
|
lastMessage, ok := messages[len(messages)-1].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected message object, got %#v", messages[len(messages)-1])
|
|
}
|
|
return lastMessage
|
|
}
|
|
|
|
func TestUpdateMovesThreadToInProgress(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
|
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
|
|
|
|
updateOut := runInboxCommand(
|
|
t,
|
|
"--db", dbPath,
|
|
"--json",
|
|
"update",
|
|
"--agent", "worker-a",
|
|
"--thread", threadID,
|
|
"--status", "in_progress",
|
|
"--summary", "Implementation started",
|
|
"--body", "Scanning current HTTP client usage.",
|
|
)
|
|
|
|
var updateResp map[string]any
|
|
mustDecodeJSON(t, updateOut, &updateResp)
|
|
if status := nestedString(t, updateResp, "data", "thread", "status"); status != "in_progress" {
|
|
t.Fatalf("expected in_progress thread status, got %q", status)
|
|
}
|
|
if kind := nestedString(t, updateResp, "data", "message", "kind"); kind != "progress" {
|
|
t.Fatalf("expected progress message kind, got %q", kind)
|
|
}
|
|
if toAgent := nestedString(t, updateResp, "data", "message", "to_agent"); toAgent != "leader" {
|
|
t.Fatalf("expected message to_agent leader, got %q", toAgent)
|
|
}
|
|
}
|
|
|
|
func TestUpdateMovesThreadToBlockedWithPayload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
|
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
|
|
|
|
updateOut := runInboxCommand(
|
|
t,
|
|
"--db", dbPath,
|
|
"--json",
|
|
"update",
|
|
"--agent", "worker-a",
|
|
"--thread", threadID,
|
|
"--status", "blocked",
|
|
"--summary", "Need timeout decision",
|
|
"--payload-json", `{"question":"Should retries apply to read timeouts?"}`,
|
|
)
|
|
|
|
var updateResp map[string]any
|
|
mustDecodeJSON(t, updateOut, &updateResp)
|
|
if status := nestedString(t, updateResp, "data", "thread", "status"); status != "blocked" {
|
|
t.Fatalf("expected blocked thread status, got %q", status)
|
|
}
|
|
if kind := nestedString(t, updateResp, "data", "message", "kind"); kind != "question" {
|
|
t.Fatalf("expected question message kind, got %q", kind)
|
|
}
|
|
payload, ok := nestedValue(t, updateResp, "data", "message", "payload_json").(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected payload_json object, got %#v", nestedValue(t, updateResp, "data", "message", "payload_json"))
|
|
}
|
|
if got := payload["question"]; got != "Should retries apply to read timeouts?" {
|
|
t.Fatalf("expected payload question, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdateAcceptsBodyFileAndArtifact(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tempDir := t.TempDir()
|
|
dbPath := filepath.Join(tempDir, "coord.db")
|
|
progressPath := filepath.Join(tempDir, "progress.md")
|
|
body := "Progress update from file."
|
|
if err := os.WriteFile(progressPath, []byte(body), 0o644); err != nil {
|
|
t.Fatalf("write progress file: %v", err)
|
|
}
|
|
|
|
threadID := seedClaimedThreadForInboxTests(t, dbPath, "leader", "worker-a", "worker-a")
|
|
|
|
runInboxCommand(
|
|
t,
|
|
"--db", dbPath,
|
|
"--json",
|
|
"update",
|
|
"--agent", "worker-a",
|
|
"--thread", threadID,
|
|
"--status", "in_progress",
|
|
"--summary", "Implementation started",
|
|
"--body-file", progressPath,
|
|
"--artifact", progressPath,
|
|
"--artifact-kind", "note",
|
|
)
|
|
|
|
showOut := runInboxCommand(
|
|
t,
|
|
"--db", dbPath,
|
|
"--json",
|
|
"show",
|
|
"--thread", threadID,
|
|
)
|
|
var showResp map[string]any
|
|
mustDecodeJSON(t, showOut, &showResp)
|
|
lastMessage := lastThreadMessageFromShow(t, showResp)
|
|
|
|
if gotBody, _ := lastMessage["body"].(string); gotBody != body {
|
|
t.Fatalf("expected body %q, got %#v", body, lastMessage["body"])
|
|
}
|
|
artifacts, ok := lastMessage["artifacts"].([]any)
|
|
if !ok || len(artifacts) != 1 {
|
|
t.Fatalf("expected one artifact, got %#v", lastMessage["artifacts"])
|
|
}
|
|
artifact, ok := artifacts[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("expected artifact object, got %#v", artifacts[0])
|
|
}
|
|
if gotPath, _ := artifact["path"].(string); gotPath != progressPath {
|
|
t.Fatalf("expected artifact path %q, got %#v", progressPath, artifact["path"])
|
|
}
|
|
if gotKind, _ := artifact["kind"].(string); gotKind != "note" {
|
|
t.Fatalf("expected artifact kind note, got %#v", artifact["kind"])
|
|
}
|
|
}
|
|
|
|
func TestUpdateRejectsInvalidPayloadJSON(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", "blocked",
|
|
"--summary", "Need timeout decision",
|
|
"--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")
|
|
}
|
|
|
|
func TestUpdateRejectsNonOwner(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-b",
|
|
"--thread", threadID,
|
|
"--status", "in_progress",
|
|
"--summary", "Implementation started",
|
|
)
|
|
if exitCode != 20 {
|
|
t.Fatalf("expected exit code 20, got %d with output %s", exitCode, stdout)
|
|
}
|
|
assertErrorJSON(t, stdout, "lease_conflict")
|
|
}
|