package httpapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "inbox/internal/base/timeutil" sqlitestore "inbox/internal/store/sqlite" ) func TestConfigAPIResolveRole(t *testing.T) { clock := timeutil.FixedClock{Time: time.Date(2026, 3, 13, 15, 0, 0, 0, time.UTC)} store, err := sqlitestore.OpenInMemory(clock) if err != nil { t.Fatalf("OpenInMemory() error = %v", err) } defer store.Close() now := timeutil.FormatRFC3339(clock.Now()) if _, err := store.DB().Exec(` INSERT INTO projects(id, slug, name, root_path, default_branch, status, created_at, updated_at) VALUES('proj_1', 'proj', 'Project', '/tmp/project', 'main', 'active', ?, ?) `, now, now); err != nil { t.Fatalf("insert project: %v", err) } if _, err := store.DB().Exec(` INSERT INTO workspaces(id, project_id, slug, name, root_path, base_branch, worktree_branch, runtime_backend, status, created_at, updated_at) VALUES('ws_1', 'proj_1', 'main', 'Main', '/tmp/project', 'main', 'worktree/main', 'local', 'active', ?, ?) `, now, now); err != nil { t.Fatalf("insert workspace: %v", err) } handler := NewHandler(store, clock) mustDoJSON(t, handler, http.MethodPut, "/api/v2/config/roles/leader", map[string]any{ "title": "Leader", "is_enabled": true, "is_builtin": true, "updated_by": "tester", }) mustDoJSON(t, handler, http.MethodPut, "/api/v2/config/roles/leader/prompts/system", map[string]any{ "content_markdown": "global-system", "updated_by": "tester", }) mustDoJSON(t, handler, http.MethodPut, "/api/v2/config/roles/leader/prompts/system", map[string]any{ "workspace_id": "ws_1", "content_markdown": "workspace-system", "updated_by": "tester", }) mustDoJSON(t, handler, http.MethodPut, "/api/v2/config/roles/leader/config", map[string]any{ "config_toml": "model = \"gpt-global\"", "auth_json": "{\"OPENAI_API_KEY\":\"token-1\"}", "updated_by": "tester", }) mustDoJSON(t, handler, http.MethodPut, "/api/v2/config/skills/clarify", map[string]any{ "name": "Clarify", "source_type": "custom", "content_markdown": "clarify skill", "status": "active", "updated_by": "tester", }) mustDoJSON(t, handler, http.MethodPut, "/api/v2/config/roles/leader/skills/clarify", map[string]any{ "workspace_id": "ws_1", "is_enabled": true, "sort_order": 5, "updated_by": "tester", }) req := httptest.NewRequest(http.MethodGet, "/api/v2/runtime/roles/leader?workspace_id=ws_1", nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("GET resolved role status = %d body=%s", rec.Code, rec.Body.String()) } var payload struct { Config struct { ConfigTOML string `json:"config_toml"` } `json:"config"` Prompts map[string]struct { ContentMarkdown string `json:"content_markdown"` } `json:"prompts"` Skills []struct { Skill struct { SkillKey string `json:"skill_key"` } `json:"skill"` } `json:"skills"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Config.ConfigTOML != "model = \"gpt-global\"" { t.Fatalf("expected global config_toml, got %q", payload.Config.ConfigTOML) } if payload.Prompts["system"].ContentMarkdown != "workspace-system" { t.Fatalf("expected workspace prompt, got %#v", payload.Prompts["system"]) } if len(payload.Skills) != 2 { t.Fatalf("expected 2 skills in payload, got %#v", payload.Skills) } if payload.Skills[0].Skill.SkillKey != "clarify" { t.Fatalf("expected workspace skill clarify first, got %#v", payload.Skills) } if payload.Skills[1].Skill.SkillKey != "inbox" { t.Fatalf("expected builtin inbox skill in payload, got %#v", payload.Skills) } } func mustDoJSON(t *testing.T, handler http.Handler, method, path string, payload map[string]any) { t.Helper() body, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal payload: %v", err) } req := httptest.NewRequest(method, path, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("%s %s status = %d body=%s", method, path, rec.Code, rec.Body.String()) } }