daemon: integrate QoL slices 1-5 (model/rename/catalog, history deletes, stats, repo prepare+imageUsed+built) — 95.3% cover green

This commit is contained in:
Raphael Westphal
2026-08-19 10:07:52 +02:00
parent 7287b831e4
commit 698b742cb3
16 changed files with 1506 additions and 36 deletions
+103
View File
@@ -0,0 +1,103 @@
package main
// api_stats_test.go — REST shape of /api/sessions/:id/stats and /api/stats.
import (
"encoding/json"
"net/http"
"testing"
)
func TestAPISessionStatsShape(t *testing.T) {
ts, store := newTestServer(t)
appendAgentEnd(t, store, "s1", 1, `{"usage":{"inputTokens":1200,"outputTokens":340,"totalCost":0.25}}`)
appendAgentEnd(t, store, "s1", 2, `{}`)
code, body := apiReq(t, http.MethodGet, ts.URL+"/api/sessions/s1/stats", testToken, "")
if code != http.StatusOK {
t.Fatalf("stats status = %d", code)
}
var stats map[string]any
if err := json.Unmarshal([]byte(body), &stats); err != nil {
t.Fatalf("decode: %v", err)
}
if stats["turns"].(float64) != 2 ||
stats["inputTokens"].(float64) != 1200 ||
stats["outputTokens"].(float64) != 340 ||
stats["totalCost"].(float64) != 0.25 {
t.Fatalf("stats = %v", stats)
}
if _, ok := stats["sessionsCount"]; ok {
t.Fatalf("per-session stats must not carry sessionsCount: %v", stats)
}
// unknown session: 200 with zeros (matches events endpoint behavior)
code, body = apiReq(t, http.MethodGet, ts.URL+"/api/sessions/ghost/stats", testToken, "")
if code != http.StatusOK {
t.Fatalf("ghost status = %d", code)
}
var ghost map[string]any
if err := json.Unmarshal([]byte(body), &ghost); err != nil {
t.Fatalf("decode ghost: %v", err)
}
if len(ghost) != 4 || ghost["turns"].(float64) != 0 {
t.Fatalf("ghost stats = %v, want four zero fields", ghost)
}
}
func TestAPIStatsTotalsAndOnlineCount(t *testing.T) {
ts, store := newTestServer(t)
appendAgentEnd(t, store, "s1", 1, `{"usage":{"inputTokens":1200000,"outputTokens":340000,"totalCost":4.2}}`)
appendAgentEnd(t, store, "s2", 1, `{"usage":{"inputTokens":50,"outputTokens":10,"totalCost":0}}`)
get := func() map[string]any {
t.Helper()
code, body := apiReq(t, http.MethodGet, ts.URL+"/api/stats", testToken, "")
if code != http.StatusOK {
t.Fatalf("/api/stats status = %d", code)
}
var m map[string]any
if err := json.Unmarshal([]byte(body), &m); err != nil {
t.Fatalf("decode: %v", err)
}
return m
}
totals := get()
if totals["turns"].(float64) != 2 ||
totals["inputTokens"].(float64) != 1200050 ||
totals["outputTokens"].(float64) != 340010 ||
totals["totalCost"].(float64) != 4.2 ||
totals["sessionsCount"].(float64) != 2 {
t.Fatalf("totals = %v", totals)
}
if totals["onlineCount"].(float64) != 0 {
t.Fatalf("onlineCount = %v, want 0 with no agents", totals["onlineCount"])
}
// one registered agent connection must show up as online
ws := dialAgent(t, ts)
if err := ws.WriteJSON(helloFrame("s-live")); err != nil {
t.Fatalf("hello: %v", err)
}
if _, _, err := ws.ReadMessage(); err != nil { // welcome
t.Fatalf("welcome: %v", err)
}
online := get()
if online["onlineCount"].(float64) != 1 {
t.Fatalf("onlineCount = %v, want 1 after hello", online["onlineCount"])
}
}
func TestAPIStatsClosedStore500(t *testing.T) {
ts, store := newTestServer(t)
if err := store.Close(); err != nil {
t.Fatalf("close store: %v", err)
}
if code, _ := apiReq(t, http.MethodGet, ts.URL+"/api/stats", testToken, ""); code != http.StatusInternalServerError {
t.Fatal("stats with broken store must 500")
}
if code, _ := apiReq(t, http.MethodGet, ts.URL+"/api/sessions/s1/stats", testToken, ""); code != http.StatusInternalServerError {
t.Fatal("session stats with broken store must 500")
}
}