package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/thepeterstone/claudomator/internal/role" ) // TestServer_ListRoleNames_ReturnsDistinctRoles verifies GET /api/roles — the // "which roles exist" discovery endpoint the role/config management panel // needs, since there's no other way to enumerate role names. func TestServer_ListRoleNames_ReturnsDistinctRoles(t *testing.T) { srv, store := testServer(t) if _, err := store.CreateRoleConfig("coder", `{"role":"coder"}`, "human"); err != nil { t.Fatalf("CreateRoleConfig coder: %v", err) } if _, err := store.CreateRoleConfig("coder", `{"role":"coder","system_prompt":"v2"}`, "human"); err != nil { t.Fatalf("CreateRoleConfig coder v2: %v", err) } if _, err := store.CreateRoleConfig("reviewer", `{"role":"reviewer"}`, "human"); err != nil { t.Fatalf("CreateRoleConfig reviewer: %v", err) } req := httptest.NewRequest("GET", "/api/roles", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var names []string if err := json.Unmarshal(w.Body.Bytes(), &names); err != nil { t.Fatalf("unmarshal: %v", err) } if len(names) != 2 || names[0] != "coder" || names[1] != "reviewer" { t.Errorf("expected [coder reviewer], got %v", names) } } func TestServer_ListRoleNames_Empty_ReturnsEmptyArray(t *testing.T) { srv, _ := testServer(t) req := httptest.NewRequest("GET", "/api/roles", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } if got := w.Body.String(); got != "[]\n" && got != "[]" { t.Errorf("expected empty JSON array, got %q", got) } } // TestServer_CreateRoleVersion_CreatesDraft mirrors the projects endpoint // test pattern (see server_test.go's testServer/httptest.NewRequest usage): // POSTing a role.RoleConfig body creates a new draft version. func TestServer_CreateRoleVersion_CreatesDraft(t *testing.T) { srv, _ := testServer(t) body, _ := json.Marshal(map[string]any{ "system_prompt": "You are a careful coder.", "escalation_ladder": []map[string]any{ { "candidates": []map[string]any{{"provider": "local", "model": "llama3.1:8b"}}, "selection_mode": "single", "max_retries": 2, }, }, }) req := httptest.NewRequest("POST", "/api/roles/coder/versions", bytes.NewReader(body)) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } var got struct { Role string `json:"role"` Version int `json:"version"` Status string `json:"status"` Config role.RoleConfig `json:"config"` } if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if got.Role != "coder" { t.Errorf("role = %q, want %q", got.Role, "coder") } if got.Version != 1 { t.Errorf("version = %d, want 1", got.Version) } if got.Status != "draft" { t.Errorf("status = %q, want draft", got.Status) } if got.Config.SystemPrompt != "You are a careful coder." { t.Errorf("system_prompt round-trip failed: got %q", got.Config.SystemPrompt) } if len(got.Config.EscalationLadder) != 1 { t.Fatalf("expected 1 tier, got %d", len(got.Config.EscalationLadder)) } } // TestServer_ListRoleVersions_ReturnsAllVersions posts two versions and // confirms the list endpoint returns both, ordered by version. func TestServer_ListRoleVersions_ReturnsAllVersions(t *testing.T) { srv, _ := testServer(t) for i := 0; i < 2; i++ { body, _ := json.Marshal(map[string]any{"system_prompt": "v"}) req := httptest.NewRequest("POST", "/api/roles/coder/versions", bytes.NewReader(body)) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create version %d: expected 201, got %d", i, w.Code) } } req := httptest.NewRequest("GET", "/api/roles/coder/versions", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var got []struct { Version int `json:"version"` Status string `json:"status"` } if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode response: %v", err) } if len(got) != 2 { t.Fatalf("expected 2 versions, got %d", len(got)) } if got[0].Version != 1 || got[1].Version != 2 { t.Errorf("expected versions [1,2], got [%d,%d]", got[0].Version, got[1].Version) } for _, v := range got { if v.Status != "draft" { t.Errorf("version %d: expected status draft, got %q", v.Version, v.Status) } } } // TestServer_ActivateRoleVersion_ActivatesAndRetiresPrior exercises the full // create -> activate -> create -> activate cycle over HTTP, confirming the // exactly-one-active-row invariant surfaces correctly through the API. func TestServer_ActivateRoleVersion_ActivatesAndRetiresPrior(t *testing.T) { srv, store := testServer(t) post := func(body string) { req := httptest.NewRequest("POST", "/api/roles/coder/versions", bytes.NewReader([]byte(body))) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Fatalf("create version: expected 201, got %d: %s", w.Code, w.Body.String()) } } post(`{"system_prompt":"v1"}`) post(`{"system_prompt":"v2"}`) activate := func(version int) int { req := httptest.NewRequest("POST", "/api/roles/coder/activate?version="+strconv.Itoa(version), nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) return w.Code } if code := activate(1); code != http.StatusOK { t.Fatalf("activate v1: expected 200, got %d", code) } active, err := store.GetActiveRoleConfig("coder") if err != nil { t.Fatalf("GetActiveRoleConfig: %v", err) } if active.Version != 1 { t.Fatalf("expected active version 1, got %d", active.Version) } if code := activate(2); code != http.StatusOK { t.Fatalf("activate v2: expected 200, got %d", code) } active, err = store.GetActiveRoleConfig("coder") if err != nil { t.Fatalf("GetActiveRoleConfig: %v", err) } if active.Version != 2 { t.Fatalf("expected active version 2 after activating it, got %d", active.Version) } versions, err := store.ListRoleConfigVersions("coder") if err != nil { t.Fatalf("ListRoleConfigVersions: %v", err) } activeCount := 0 for _, v := range versions { if v.Status == "active" { activeCount++ } } if activeCount != 1 { t.Fatalf("expected exactly 1 active version, got %d", activeCount) } } // TestServer_ActivateRoleVersion_UnknownVersion_Returns400 confirms // activating a version that doesn't exist is a client error, not a panic or // 500. func TestServer_ActivateRoleVersion_UnknownVersion_Returns400(t *testing.T) { srv, _ := testServer(t) req := httptest.NewRequest("POST", "/api/roles/coder/activate?version=99", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } }