package api import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/will/k8s-agent-dashboard/internal/claude" ) type fakeLoader struct{} func (f fakeLoader) ClaudeDir() string { return "/tmp/claude" } func (f fakeLoader) LoadStatsCache() (*claude.StatsCache, error) { return &claude.StatsCache{TotalSessions: 3}, nil } func (f fakeLoader) ListDir(name string) ([]claude.DirEntry, error) { return nil, nil } func (f fakeLoader) FileMeta(relPath string) (claude.FileMeta, error) { return claude.FileMeta{}, nil } func (f fakeLoader) PathExists(relPath string) bool { return true } func TestGetClaudeStats(t *testing.T) { r := chi.NewRouter() r.Get("/api/claude/stats", GetClaudeStats(fakeLoader{})) req := httptest.NewRequest(http.MethodGet, "/api/claude/stats", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 200 { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } } func TestGetClaudeSummary_IncludesDerivedCostSignals(t *testing.T) { r := chi.NewRouter() r.Get("/api/claude/summary", GetClaudeSummary(fakeSummaryLoader{})) req := httptest.NewRequest(http.MethodGet, "/api/claude/summary", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != 200 { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } // Smoke-check that derived fields exist. body := w.Body.String() for _, want := range []string{"cacheHitRatioEstimate", "topModelByOutputTokens"} { if !jsonContainsKey(body, want) { t.Fatalf("expected response to include key %s, body=%s", want, body) } } } type fakeSummaryLoader struct{ fakeLoader } func (f fakeSummaryLoader) LoadStatsCache() (*claude.StatsCache, error) { return &claude.StatsCache{ TotalSessions: 3, TotalMessages: 10, ModelUsage: map[string]claude.ModelUsage{ "claude-3-5-sonnet": { InputTokens: 100, OutputTokens: 250, CacheReadInputTokens: 50, CacheCreationInputTokens: 25, }, "claude-3-5-haiku": { InputTokens: 80, OutputTokens: 300, CacheReadInputTokens: 20, }, }, }, nil } func jsonContainsKey(body, key string) bool { var m map[string]any if err := json.Unmarshal([]byte(body), &m); err != nil { return false } return mapContainsKey(m, key) } func mapContainsKey(v any, key string) bool { switch vv := v.(type) { case map[string]any: if _, ok := vv[key]; ok { return true } for _, child := range vv { if mapContainsKey(child, key) { return true } } case []any: for _, child := range vv { if mapContainsKey(child, key) { return true } } } return false }