summaryrefslogtreecommitdiff
path: root/internal/api/dashboard_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/dashboard_test.go')
-rw-r--r--internal/api/dashboard_test.go131
1 files changed, 131 insertions, 0 deletions
diff --git a/internal/api/dashboard_test.go b/internal/api/dashboard_test.go
new file mode 100644
index 0000000..2b80de3
--- /dev/null
+++ b/internal/api/dashboard_test.go
@@ -0,0 +1,131 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/thepeterstone/claudomator/internal/storage"
+ "github.com/thepeterstone/claudomator/internal/task"
+)
+
+func TestHandleGetEscalationFunnel_AggregatesWithinDefaultWindow(t *testing.T) {
+ srv, store := testServer(t)
+ tk := createTaskWithState(t, store, "funnel-task", task.StateCompleted)
+
+ now := time.Now().UTC()
+ mustCreateAPIExecution(t, store, "f1", tk.ID, "local", 0, 0, now.Add(-1*time.Hour))
+ mustCreateAPIExecution(t, store, "f2", tk.ID, "local", 0, 0, now.Add(-2*time.Hour))
+ mustCreateAPIExecution(t, store, "f3", tk.ID, "anthropic", 1, 0.10, now.Add(-3*time.Hour))
+ // Outside the default 24h window.
+ mustCreateAPIExecution(t, store, "f4", tk.ID, "local", 0, 0, now.Add(-48*time.Hour))
+
+ req := httptest.NewRequest("GET", "/api/escalation-funnel", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: want 200, got %d: %s", w.Code, w.Body.String())
+ }
+ var buckets []storage.EscalationBucket
+ if err := json.Unmarshal(w.Body.Bytes(), &buckets); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(buckets) != 2 {
+ t.Fatalf("expected 2 buckets (rung 0/local, rung 1/anthropic), got %d: %+v", len(buckets), buckets)
+ }
+ for _, b := range buckets {
+ if b.Rung == 0 && b.Agent == "local" && b.Count != 2 {
+ t.Errorf("rung0/local count = %d, want 2", b.Count)
+ }
+ if b.Rung == 1 && b.Agent == "anthropic" && (b.Count != 1 || b.CostUSD != 0.10) {
+ t.Errorf("rung1/anthropic = %+v, want count 1 cost 0.10", b)
+ }
+ }
+}
+
+func TestHandleGetEscalationFunnel_WindowParam_Narrows(t *testing.T) {
+ srv, store := testServer(t)
+ tk := createTaskWithState(t, store, "funnel-window-task", task.StateCompleted)
+ now := time.Now().UTC()
+ mustCreateAPIExecution(t, store, "fw1", tk.ID, "local", 0, 0, now.Add(-10*time.Hour))
+
+ req := httptest.NewRequest("GET", "/api/escalation-funnel?window=5h", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ var buckets []storage.EscalationBucket
+ if err := json.Unmarshal(w.Body.Bytes(), &buckets); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(buckets) != 0 {
+ t.Errorf("expected no buckets within a 5h window, got %+v", buckets)
+ }
+}
+
+func TestHandleGetSpendTimeseries_ReturnsPerProviderPoints(t *testing.T) {
+ srv, store := testServer(t)
+ tk := createTaskWithState(t, store, "spend-task", task.StateCompleted)
+ now := time.Now().UTC()
+ mustCreateAPIExecution(t, store, "s1", tk.ID, "local", 0, 0.01, now.Add(-1*time.Hour))
+ mustCreateAPIExecution(t, store, "s2", tk.ID, "anthropic", 0, 0.20, now.Add(-1*time.Hour))
+
+ req := httptest.NewRequest("GET", "/api/spend-timeseries", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: want 200, got %d: %s", w.Code, w.Body.String())
+ }
+ var points []storage.SpendPoint
+ if err := json.Unmarshal(w.Body.Bytes(), &points); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ agents := map[string]bool{}
+ for _, p := range points {
+ agents[p.Agent] = true
+ }
+ if !agents["local"] || !agents["anthropic"] {
+ t.Errorf("expected points for both local and anthropic, got %+v", points)
+ }
+}
+
+func TestHandleGetSpendTimeseries_EmptyDB_ReturnsEmptyArray(t *testing.T) {
+ srv, _ := testServer(t)
+ req := httptest.NewRequest("GET", "/api/spend-timeseries", nil)
+ w := httptest.NewRecorder()
+ srv.Handler().ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status: want 200, got %d", w.Code)
+ }
+ var points []storage.SpendPoint
+ if err := json.Unmarshal(w.Body.Bytes(), &points); err != nil {
+ t.Fatalf("unmarshal: %v", err)
+ }
+ if len(points) != 0 {
+ t.Errorf("expected empty slice, got %+v", points)
+ }
+}
+
+// mustCreateAPIExecution mirrors createExecution (executions_test.go) but
+// also sets Agent/EscalationRung, which the funnel/spend-timeseries
+// endpoints aggregate on.
+func mustCreateAPIExecution(t *testing.T, store *storage.DB, id, taskID, agent string, rung int, cost float64, start time.Time) {
+ t.Helper()
+ exec := &storage.Execution{
+ ID: id,
+ TaskID: taskID,
+ StartTime: start,
+ EndTime: start.Add(time.Minute),
+ Status: "COMPLETED",
+ CostUSD: cost,
+ Agent: agent,
+ EscalationRung: rung,
+ }
+ if err := store.CreateExecution(exec); err != nil {
+ t.Fatalf("createExecution(%s): %v", id, err)
+ }
+}