package api import ( "bytes" "database/sql" "encoding/json" "net/http" "net/http/httptest" "testing" "work-queue-api/internal/db" ) func newTestServer(t *testing.T) (*Server, *sql.DB) { t.Helper() sqliteDB, err := db.Open(t.TempDir() + "/test.db") if err != nil { t.Fatalf("open db: %v", err) } server, err := NewServer(sqliteDB) if err != nil { t.Fatalf("new server: %v", err) } return server, sqliteDB } func doJSON(t *testing.T, h http.Handler, method, path string, body any) *httptest.ResponseRecorder { t.Helper() var payload []byte if body != nil { var err error payload, err = json.Marshal(body) if err != nil { t.Fatalf("marshal body: %v", err) } } req := httptest.NewRequest(method, path, bytes.NewReader(payload)) res := httptest.NewRecorder() h.ServeHTTP(res, req) return res } func TestProjectAndWorkLifecycle(t *testing.T) { server, sqliteDB := newTestServer(t) defer sqliteDB.Close() router := server.Router() projectRes := doJSON(t, router, http.MethodPost, "/projects", map[string]any{ "name": "Shopping List API", "external_ref": "todoist:123", }) if projectRes.Code != http.StatusCreated { t.Fatalf("create project status = %d body=%s", projectRes.Code, projectRes.Body.String()) } var project map[string]any if err := json.Unmarshal(projectRes.Body.Bytes(), &project); err != nil { t.Fatalf("decode project: %v", err) } workRes := doJSON(t, router, http.MethodPost, "/work", map[string]any{ "project_id": project["id"], "type": "code_review", "description": "Review PR #3", "payload": map[string]any{"pr": 3}, "priority": 2, "assigned_agent": "steve-w", "created_by": "marcus-a", }) if workRes.Code != http.StatusCreated { t.Fatalf("create work status = %d body=%s", workRes.Code, workRes.Body.String()) } var work map[string]any if err := json.Unmarshal(workRes.Body.Bytes(), &work); err != nil { t.Fatalf("decode work: %v", err) } for _, step := range []map[string]any{ {"status": "dispatched", "assigned_agent": "steve-w"}, {"status": "in_progress"}, {"status": "completed", "outcome": "success", "notes": "done"}, } { res := doJSON(t, router, http.MethodPatch, "/work/"+work["id"].(string), step) if res.Code != http.StatusOK { t.Fatalf("patch work step=%v status = %d body=%s", step, res.Code, res.Body.String()) } } getRes := doJSON(t, router, http.MethodGet, "/work/"+work["id"].(string), nil) if getRes.Code != http.StatusOK { t.Fatalf("get work status = %d body=%s", getRes.Code, getRes.Body.String()) } var fetched map[string]any if err := json.Unmarshal(getRes.Body.Bytes(), &fetched); err != nil { t.Fatalf("decode fetched work: %v", err) } if fetched["status"] != "completed" { t.Fatalf("expected completed status, got %v", fetched["status"]) } logs, ok := fetched["dispatch_log"].([]any) if !ok || len(logs) != 1 { t.Fatalf("expected 1 dispatch log, got %v", fetched["dispatch_log"]) } } func TestOneInProgressPerAgent(t *testing.T) { server, sqliteDB := newTestServer(t) defer sqliteDB.Close() router := server.Router() create := func(desc string) string { res := doJSON(t, router, http.MethodPost, "/work", map[string]any{ "type": "bug_fix", "description": desc, "assigned_agent": "steve-w", }) if res.Code != http.StatusCreated { t.Fatalf("create work status = %d body=%s", res.Code, res.Body.String()) } var body map[string]any _ = json.Unmarshal(res.Body.Bytes(), &body) return body["id"].(string) } first := create("first") second := create("second") for _, id := range []string{first, second} { res := doJSON(t, router, http.MethodPatch, "/work/"+id, map[string]any{"status": "dispatched", "assigned_agent": "steve-w"}) if res.Code != http.StatusOK { t.Fatalf("dispatch status = %d body=%s", res.Code, res.Body.String()) } } res := doJSON(t, router, http.MethodPatch, "/work/"+first, map[string]any{"status": "in_progress"}) if res.Code != http.StatusOK { t.Fatalf("first in progress status = %d body=%s", res.Code, res.Body.String()) } res = doJSON(t, router, http.MethodPatch, "/work/"+second, map[string]any{"status": "in_progress"}) if res.Code != http.StatusConflict { t.Fatalf("expected 409, got %d body=%s", res.Code, res.Body.String()) } }