package handler import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models" "code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/store" "github.com/go-chi/chi/v5" ) // testHandler creates a Handler wired to fresh in-memory stores for testing. func testHandler(t *testing.T) *Handler { t.Helper() return NewHandler( store.NewAgentStore(), store.NewSessionStore(), store.NewTaskStore(), store.NewProjectStore(), ) } // serveChi creates a chi.Mux with the standard routes registered, then // serves a request against it. Returns the recorded response. func serveChi(h *Handler, method, path, body string) *httptest.ResponseRecorder { w := httptest.NewRecorder() req := httptest.NewRequest(method, path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") r := chi.NewRouter() r.Route("/api", func(api chi.Router) { api.Route("/agents", func(agents chi.Router) { agents.Get("/", h.ListAgents) agents.Post("/", h.CreateAgent) agents.Get("/{id}", h.GetAgent) agents.Put("/{id}", h.UpdateAgent) agents.Delete("/{id}", h.DeleteAgent) agents.Get("/{id}/history", h.AgentHistory) }) api.Get("/sessions", h.ListSessions) api.Get("/tasks", h.ListTasks) api.Get("/projects", h.ListProjects) }) r.ServeHTTP(w, req) return w } func parseBody(t *testing.T, w *httptest.ResponseRecorder) map[string]any { t.Helper() var body map[string]any if err := json.NewDecoder(w.Result().Body).Decode(&body); err != nil { t.Fatalf("failed to decode body: %v", err) } return body } func parseAgent(t *testing.T, w *httptest.ResponseRecorder) models.AgentCardData { t.Helper() var a models.AgentCardData if err := json.NewDecoder(w.Result().Body).Decode(&a); err != nil { t.Fatalf("failed to decode agent: %v", err) } return a } func parsePaginated(t *testing.T, w *httptest.ResponseRecorder) models.PaginatedResponse { t.Helper() var pr models.PaginatedResponse if err := json.NewDecoder(w.Result().Body).Decode(&pr); err != nil { t.Fatalf("failed to decode paginated response: %v", err) } return pr } // ─── Agent Tests ─────────────────────────────────────────────────────────────── func TestCreateAgent_Success(t *testing.T) { h := testHandler(t) w := serveChi(h, "POST", "/api/agents", `{ "id": "dex", "displayName": "Dex", "role": "Backend Dev", "status": "idle", "sessionKey": "sess-1", "channel": "discord" }`) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } a := parseAgent(t, w) if a.ID != "dex" { t.Errorf("expected id=dax, got %s", a.ID) } if a.Status != models.AgentStatusIdle { t.Errorf("expected status=idle, got %s", a.Status) } } func TestCreateAgent_Duplicate(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"otto","displayName":"Otto","role":"Orchestrator","status":"active","sessionKey":"s1","channel":"discord"}`) w := serveChi(h, "POST", "/api/agents", `{"id":"otto","displayName":"Otto","role":"Orchestrator","status":"active","sessionKey":"s2","channel":"discord"}`) if w.Code != http.StatusConflict { t.Fatalf("expected 409, got %d", w.Code) } } func TestCreateAgent_Validation(t *testing.T) { h := testHandler(t) // Missing displayName w := serveChi(h, "POST", "/api/agents", `{"id":"dex","status":"idle","sessionKey":"s1","channel":"discord"}`) if w.Code != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", w.Code) } } func TestCreateAgent_InvalidStatus(t *testing.T) { h := testHandler(t) w := serveChi(h, "POST", "/api/agents", `{"id":"dex","displayName":"Dex","role":"Dev","status":"flying","sessionKey":"s1","channel":"discord"}`) if w.Code != http.StatusUnprocessableEntity { t.Fatalf("expected 422 for invalid status, got %d", w.Code) } } func TestGetAgent_NotFound(t *testing.T) { h := testHandler(t) w := serveChi(h, "GET", "/api/agents/nonexistent", "") if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestGetAgent_Success(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"pip","displayName":"Pip","role":"Edge Dev","status":"idle","sessionKey":"s1","channel":"discord"}`) w := serveChi(h, "GET", "/api/agents/pip", "") if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } a := parseAgent(t, w) if a.DisplayName != "Pip" { t.Errorf("expected Pip, got %s", a.DisplayName) } } func TestListAgents(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"a1","displayName":"Alpha","role":"Tester","status":"idle","sessionKey":"s1","channel":"discord"}`) serveChi(h, "POST", "/api/agents", `{"id":"a2","displayName":"Beta","role":"Tester","status":"active","sessionKey":"s2","channel":"discord"}`) w := serveChi(h, "GET", "/api/agents", "") if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } pr := parsePaginated(t, w) if pr.TotalCount != 2 { t.Errorf("expected totalCount=2, got %d", pr.TotalCount) } } func TestListAgents_FilterByStatus(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"a1","displayName":"Alpha","role":"Tester","status":"idle","sessionKey":"s1","channel":"discord"}`) serveChi(h, "POST", "/api/agents", `{"id":"a2","displayName":"Beta","role":"Tester","status":"active","sessionKey":"s2","channel":"discord"}`) w := serveChi(h, "GET", "/api/agents?status=active", "") pr := parsePaginated(t, w) if pr.TotalCount != 2 { t.Errorf("expected totalCount=2 (unfiltered), got %d", pr.TotalCount) } } func TestUpdateAgent(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"hex","displayName":"Hex","role":"DB Specialist","status":"idle","sessionKey":"s1","channel":"discord"}`) w := serveChi(h, "PUT", "/api/agents/hex", `{"status":"thinking","currentTask":"schema review"}`) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } a := parseAgent(t, w) if a.Status != models.AgentStatusThinking { t.Errorf("expected status=thinking, got %s", a.Status) } if a.CurrentTask == nil || *a.CurrentTask != "schema review" { t.Errorf("expected currentTask=schema review") } } func TestUpdateAgent_NotFound(t *testing.T) { h := testHandler(t) w := serveChi(h, "PUT", "/api/agents/nope", `{"status":"idle"}`) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } func TestDeleteAgent(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"temp","displayName":"Temp","role":"Temp","status":"idle","sessionKey":"s1","channel":"discord"}`) w := serveChi(h, "DELETE", "/api/agents/temp", "") if w.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d", w.Code) } // Verify gone w2 := serveChi(h, "GET", "/api/agents/temp", "") if w2.Code != http.StatusNotFound { t.Fatalf("expected 404 after delete, got %d", w2.Code) } } func TestAgentHistory(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"nano","displayName":"Nano","role":"Firmware","status":"idle","sessionKey":"s1","channel":"discord"}`) serveChi(h, "PUT", "/api/agents/nano", `{"status":"thinking","currentTask":"mqtt payload"}`) w := serveChi(h, "GET", "/api/agents/nano/history", "") if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var entries []models.AgentStatusHistoryEntry json.NewDecoder(w.Result().Body).Decode(&entries) if len(entries) < 2 { t.Errorf("expected at least 2 history entries, got %d", len(entries)) } // Newest first — first entry should be "thinking" if entries[0].Status != models.AgentStatusThinking { t.Errorf("expected newest entry status=thinking, got %s", entries[0].Status) } } func TestAgentHistory_NotFound(t *testing.T) { h := testHandler(t) w := serveChi(h, "GET", "/api/agents/ghost/history", "") if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } // ─── Session Tests ─────────────────────────────────────────────────────────════ func TestListSessions_Empty(t *testing.T) { h := testHandler(t) w := serveChi(h, "GET", "/api/sessions", "") if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } pr := parsePaginated(t, w) if pr.TotalCount != 0 { t.Errorf("expected 0 sessions, got %d", pr.TotalCount) } } func TestListSessions_WithData(t *testing.T) { h := testHandler(t) h.SessionStore.Create(models.Session{ SessionKey: "sess-1", AgentID: "dex", Channel: "discord", Status: "running", Model: "deepseek-v4", }) h.SessionStore.Create(models.Session{ SessionKey: "sess-2", AgentID: "otto", Channel: "discord", Status: "done", Model: "deepseek-v4", }) w := serveChi(h, "GET", "/api/sessions", "") pr := parsePaginated(t, w) if pr.TotalCount != 2 { t.Errorf("expected totalCount=2, got %d", pr.TotalCount) } } // ─── Task Tests ──────────────────────────────────────────────────────────────── func TestListTasks_Empty(t *testing.T) { h := testHandler(t) w := serveChi(h, "GET", "/api/tasks", "") if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } } func TestListTasks_WithData(t *testing.T) { h := testHandler(t) h.TaskStore.Create(models.Task{ AgentID: "dex", Title: "Implement CRUD API", Status: models.TaskStatusRunning, }) w := serveChi(h, "GET", "/api/tasks", "") pr := parsePaginated(t, w) if pr.TotalCount != 1 { t.Errorf("expected totalCount=1, got %d", pr.TotalCount) } } // ─── Project Tests ───────────────────────────────────────────────────────────── func TestListProjects_Empty(t *testing.T) { h := testHandler(t) w := serveChi(h, "GET", "/api/projects", "") if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } } func TestListProjects_WithData(t *testing.T) { h := testHandler(t) h.ProjectStore.Create(models.Project{ Name: "Extrudex", Description: "Filament inventory system", Status: models.ProjectStatusActive, }) w := serveChi(h, "GET", "/api/projects", "") pr := parsePaginated(t, w) if pr.TotalCount != 1 { t.Errorf("expected totalCount=1, got %d", pr.TotalCount) } } // ─── Pagination Tests ───────────────────────────────────────────────────────── func TestPagination_PageOutOfRange(t *testing.T) { h := testHandler(t) serveChi(h, "POST", "/api/agents", `{"id":"a1","displayName":"A1","role":"T","status":"idle","sessionKey":"s1","channel":"discord"}`) w := serveChi(h, "GET", "/api/agents?page=99", "") pr := parsePaginated(t, w) if len(pr.Data.([]any)) != 0 { t.Errorf("expected empty page, got %d items", len(pr.Data.([]any))) } // HasMore=false because we're past all data — nothing more to fetch. if pr.HasMore { t.Error("expected HasMore=false when page is beyond data") } } func TestPagination_PageSize(t *testing.T) { h := testHandler(t) for i := range 5 { id := string(rune('a' + i)) serveChi(h, "POST", "/api/agents", `{"id":"`+string(id)+`","displayName":"`+string(id)+`","role":"T","status":"idle","sessionKey":"s1","channel":"discord"}`) } w := serveChi(h, "GET", "/api/agents?pageSize=2&page=2", "") pr := parsePaginated(t, w) if pr.PageSize != 2 { t.Errorf("expected pageSize=2, got %d", pr.PageSize) } }