package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) func ingestUsage(t *testing.T, s *cloudServer, token string, rows []botUsageRow) (int, map[string]any) { t.Helper() return ingestUsageReport(t, s, token, "bbbbbbbbbbbbbbbb", rows) } func ingestUsageReport(t *testing.T, s *cloudServer, token, reportID string, rows []botUsageRow) (int, map[string]any) { t.Helper() raw, _ := json.Marshal(map[string]any{"report_id": reportID, "rows": rows}) req := httptest.NewRequest("POST", "/api/usage", bytes.NewReader(raw)) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rec := httptest.NewRecorder() s.usageHandler(rec, req) var body map[string]any json.NewDecoder(rec.Body).Decode(&body) return rec.Code, body } // Ingest: the meter token resolves to exactly one space, rows upsert // additively (partial-day reports accumulate), and a stranger is refused. func TestUsageIngest(t *testing.T) { db := openTestCloudDB(t) id := insertSpaceRow(t, db, "space.11ch.at", 1000) if err := db.setMeterTokenHash(id, hashToken("meter-tok")); err != nil { t.Fatal(err) } s := &cloudServer{db: db} rows := []botUsageRow{ {Day: "2026-07-14", Model: "claude-sonnet-5", Dimension: "llm_input_tokens", Quantity: 100}, {Day: "2026-07-14", Dimension: "charter_runs", Quantity: 2}, } if code, body := ingestUsageReport(t, s, "meter-tok", "aa11aa11aa11aa11", rows); code != http.StatusOK { t.Fatalf("ingest: %d %v", code, body) } // A second partial-day report (new id) adds, never clobbers. if code, _ := ingestUsageReport(t, s, "meter-tok", "aa22aa22aa22aa22", rows[:1]); code != http.StatusOK { t.Fatal("second ingest failed") } // A REPLAY (same id — the ack-lost retry) is recognized and dropped whole. if code, _ := ingestUsageReport(t, s, "meter-tok", "aa11aa11aa11aa11", rows); code != http.StatusOK { t.Fatal("replay must still answer ok") } got, err := db.usageFor(id) if err != nil { t.Fatal(err) } byKey := map[string]int64{} for _, r := range got { byKey[r.Model+"|"+r.Dimension] = r.Quantity } if byKey["claude-sonnet-5|llm_input_tokens"] != 200 || byKey["|charter_runs"] != 2 { t.Fatalf("ledger = %v (replay must not double-count)", byKey) } // Prune drops old dedupe ids; a post-prune replay would re-count, which // is why the window is days against a minutes-scale retry horizon. if err := db.pruneUsageReports(time.Now().Unix() + 1); err != nil { t.Fatal(err) } if code, _ := ingestUsage(t, s, "wrong", rows); code != http.StatusForbidden { t.Fatal("bad token must be refused") } if code, _ := ingestUsage(t, s, "", rows); code != http.StatusForbidden { t.Fatal("missing token must be refused") } // An unmetered space's empty hash must never match any presented token. other := insertSpaceRow(t, db, "other.11ch.at", 1000) if sid, ok := db.spaceByMeterToken(""); ok { t.Fatalf("empty token resolved to space %d", sid) } _ = other if code, _ := ingestUsage(t, s, "meter-tok", []botUsageRow{{Day: "bad", Dimension: "llm_requests", Quantity: 1}}); code != http.StatusBadRequest { t.Fatal("invalid rows must be refused at the ledger too") } } // The sweep mints a meter token for any space without one, pushes it through // controld, and records only the hash. A space that already has one is left // alone — tokens rotate only when a push failed to record. func TestMeterSweepMintsTokens(t *testing.T) { db := openTestCloudDB(t) id := insertSpaceRow(t, db, "fresh.11ch.at", 1000) var pushes int var gotURL, gotToken string mux := http.NewServeMux() mux.HandleFunc("GET /instances/{host}/stats", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"storage_bytes": 1, "setup_complete": true, "members": 1}) }) mux.HandleFunc("POST /instances/{host}/meter", func(w http.ResponseWriter, r *http.Request) { var body struct{ URL, Token string } json.NewDecoder(r.Body).Decode(&body) pushes++ gotURL, gotToken = body.URL, body.Token writeJSON(w, http.StatusOK, map[string]any{"ok": true}) }) sock := fakeControl(t, mux.ServeHTTP) s := &cloudServer{db: db, control: controlClient(sock), controlToken: "t", usageURL: "https://cloud.example/api/usage"} s.meterOnce() if pushes != 1 || gotURL != "https://cloud.example/api/usage" || gotToken == "" { t.Fatalf("push: n=%d url=%q token-empty=%v", pushes, gotURL, gotToken == "") } // The pushed token authenticates; only its hash is stored. if sid, ok := db.spaceByMeterToken(gotToken); !ok || sid != id { t.Fatalf("pushed token does not resolve: ok=%v sid=%d", ok, sid) } // Converged: the next sweep must not re-mint. s.meterOnce() if pushes != 1 { t.Fatalf("second sweep re-minted (pushes=%d)", pushes) } } // With the usage pipe off (no LCHAT_CLOUD_USAGE_URL) the sweep never touches // meter tokens — self-hosted cloud deployments see no behavior change. func TestMeterSweepNoUsageURL(t *testing.T) { db := openTestCloudDB(t) insertSpaceRow(t, db, "fresh.11ch.at", 1000) mux := http.NewServeMux() mux.HandleFunc("GET /instances/{host}/stats", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"storage_bytes": 1}) }) mux.HandleFunc("POST /instances/{host}/meter", func(w http.ResponseWriter, r *http.Request) { t.Error("meter push must not happen with the pipe off") }) sock := fakeControl(t, mux.ServeHTTP) s := &cloudServer{db: db, control: controlClient(sock), controlToken: "t"} s.meterOnce() }