summaryrefslogtreecommitdiff
path: root/internal/api/budget_test.go
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-26 20:34:09 +0000
committerClaude <noreply@anthropic.com>2026-05-26 20:34:09 +0000
commitab4b364954af08fa602388495ca425eaef0abf74 (patch)
treec0806a57295270b4d86f097c3d0b6a0e1d631201 /internal/api/budget_test.go
parent32715355fe2eed321df4f7083dfe580d35f8a62a (diff)
feat(api,web): budget headroom endpoint + UI chips (Phase 6)
Adds GET /api/budget returning per-provider rolling-window headroom (empty when budget gating is unconfigured), wired from serve.go via SetBudget. The web UI polls it and renders a chip per limited provider in the header, flagging any under 20% remaining. New pure JS helpers formatBudgetHeadroom/ renderBudgetHeadroom are unit-tested; the endpoint is covered by Go handler tests (empty/reports/500). UI render not browser-verified in this environment. Completes Phase 6: spend accounting + dispatcher gating + observability surface. https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
Diffstat (limited to 'internal/api/budget_test.go')
-rw-r--r--internal/api/budget_test.go68
1 files changed, 68 insertions, 0 deletions
diff --git a/internal/api/budget_test.go b/internal/api/budget_test.go
new file mode 100644
index 0000000..d59836d
--- /dev/null
+++ b/internal/api/budget_test.go
@@ -0,0 +1,68 @@
+package api
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/thepeterstone/claudomator/internal/budget"
+)
+
+type fakeBudget struct {
+ hs []budget.Headroom
+ err error
+}
+
+func (f fakeBudget) All() ([]budget.Headroom, error) { return f.hs, f.err }
+
+func TestHandleGetBudget_NoBudgetConfigured_ReturnsEmpty(t *testing.T) {
+ srv, _ := testServer(t)
+ req := httptest.NewRequest("GET", "/api/budget", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: want 200, got %d", w.Code)
+ }
+ var hs []budget.Headroom
+ if err := json.Unmarshal(w.Body.Bytes(), &hs); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(hs) != 0 {
+ t.Errorf("want empty list, got %+v", hs)
+ }
+}
+
+func TestHandleGetBudget_ReportsHeadroom(t *testing.T) {
+ srv, _ := testServer(t)
+ srv.SetBudget(fakeBudget{hs: []budget.Headroom{
+ {Provider: "claude", Limited: true, Limit: 10, Spent: 4, Remaining: 6, Fraction: 0.6},
+ }})
+ req := httptest.NewRequest("GET", "/api/budget", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: want 200, got %d", w.Code)
+ }
+ var hs []budget.Headroom
+ if err := json.Unmarshal(w.Body.Bytes(), &hs); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(hs) != 1 || hs[0].Provider != "claude" || hs[0].Remaining != 6 {
+ t.Errorf("unexpected headroom: %+v", hs)
+ }
+}
+
+func TestHandleGetBudget_ErrorReturns500(t *testing.T) {
+ srv, _ := testServer(t)
+ srv.SetBudget(fakeBudget{err: errors.New("db down")})
+ req := httptest.NewRequest("GET", "/api/budget", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+ if w.Code != http.StatusInternalServerError {
+ t.Errorf("want 500, got %d", w.Code)
+ }
+}