summaryrefslogtreecommitdiff
path: root/internal/api/roles_test.go
diff options
context:
space:
mode:
authorClaude Sonnet 5 <noreply@anthropic.com>2026-07-04 08:50:46 +0000
committerClaude Sonnet 5 <noreply@anthropic.com>2026-07-04 08:50:46 +0000
commitf8ae821240f33d615a9e91cdfeb6c026b7970782 (patch)
tree458ce067db47a92248f163c26ce5fcf71299f3b0 /internal/api/roles_test.go
parentd105eca610b0e737c3313e4978d6a917b4f55d10 (diff)
feat(web,api): add Budget/Roles dashboard -- final phase of the harness redesign (Phase 9b)
Two new tabs, matching Phase 9a's conventions: Budget (per-provider spend meters, escalation funnel, spend-over-time) and Roles (version history, draft activation, readable escalation-ladder view) -- the human-facing surface for the token-husbanding value proposition and the Phase 5/8 versioned role-config system. New backend (minimal, additive, matching existing endpoint conventions -- unauthenticated like /api/budget and /api/roles/*): - internal/storage/dashboard.go: QueryEscalationFunnel (executions grouped by escalation_rung + agent, count + cost) and QuerySpendTimeseries (cost per provider bucketed hourly/daily, mirroring QueryDashboardStats' existing bucketing expressions). Documented honestly: rung 0 is not exclusively "resolved locally" -- escalation_rung defaults to 0 uniformly, so non-role-typed executions (which never climb a ladder) land there too, alongside role-typed tasks genuinely resolved at tier 0. A role-only variant would need a join against tasks.config_json with no queryable role column on executions -- intentionally out of scope. - internal/api/dashboard.go: GET /api/escalation-funnel, GET /api/spend-timeseries (both ?window=5h|24h|7d|<duration>, default 24h). - internal/storage.ListRoleNames + GET /api/roles: the "which roles exist" gap -- there was no way to discover role names before this, only to list versions for a role you already knew the name of. Chart-form decisions (dataviz skill, invoked before writing chart code): horizontal stacked bar for the escalation funnel (rung order already encodes the funnel shape positionally; color only needed for per-provider segments within each rung); multi-line for spend-over-time; a fixed-order categorical palette from the skill's validated palette.md slots for provider identity (re-validated against this app's dark surface, passing); a separate status (good/warning/critical) palette for budget meters, deliberately distinct from both --state-* and the provider palette. Caught and fixed a real bug during visual QA: converging near-zero end-labels on the spend chart were overlapping (an anti-pattern the skill explicitly flags) -- fixed with a 14px minimum-gap check before direct-labeling an endpoint, leaning on the legend/tooltip otherwise. Verified with a real running server, real seeded data (65 executions across rungs 0-2 with a realistic provider mix, 2 roles with active/draft/retired role_configs versions written directly via internal/storage), and a real headless-browser session (reusing Phase 9a's Chromium/proxy scaffinding): confirmed correct rung totals/percentages, provider legends, a 3-line spend chart, live window-selector re-render, correct active-version highlighting on the role panel, and a real Activate click on a draft version -- verified via both DOM re-render and a direct backend GET that it truly persisted. go build/vet/test -race -count=1 all pass, full suite. node --test web/test/*.mjs: 291/291 passing (16 new). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'internal/api/roles_test.go')
-rw-r--r--internal/api/roles_test.go46
1 files changed, 46 insertions, 0 deletions
diff --git a/internal/api/roles_test.go b/internal/api/roles_test.go
index 97afdfe..1f17426 100644
--- a/internal/api/roles_test.go
+++ b/internal/api/roles_test.go
@@ -11,6 +11,52 @@ import (
"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.