Add comprehensive Claude Code monitoring and realtime streaming to the K8s dashboard. Includes API endpoints for health, stats, summary, inventory, and live event streaming. Frontend provides overview, usage, inventory, debug, and live feed views.
111 lines
2.6 KiB
Go
111 lines
2.6 KiB
Go
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
|
|
}
|