package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" ) func postUsage(t *testing.T, base, token string, rows []botUsageRow) (int, map[string]any) { t.Helper() return postUsageReport(t, base, token, "aaaaaaaaaaaaaaaa", rows) } func postUsageReport(t *testing.T, base, token, reportID string, rows []botUsageRow) (int, map[string]any) { t.Helper() raw, _ := json.Marshal(map[string]any{"token": token, "report_id": reportID, "rows": rows}) resp, err := http.Post(base+"/api/bot-usage", "application/json", bytes.NewReader(raw)) if err != nil { t.Fatal(err) } defer resp.Body.Close() var body map[string]any json.NewDecoder(resp.Body).Decode(&body) return resp.StatusCode, body } // The relay end to end: a bot's report is validated, forwarded to the cloud // meter with the instance's bearer token, and the cloud's budget state comes // back to the sidecar untouched. func TestBotUsageRelay(t *testing.T) { ts, s := newTestServer(t) agent := provisionNamed(t, ts.URL, "Eleven", true) var gotAuth string var gotRows []botUsageRow cloud := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") var body struct { Rows []botUsageRow `json:"rows"` } json.NewDecoder(r.Body).Decode(&body) gotRows = body.Rows writeJSON(w, http.StatusOK, map[string]any{"ok": true, "budget": map[string]any{"state": "low"}}) })) defer cloud.Close() s.meterURL, s.meterToken = cloud.URL, "meter-tok" rows := []botUsageRow{ {Day: "2026-07-14", Model: "claude-sonnet-5", Dimension: "llm_input_tokens", Quantity: 150}, {Day: "2026-07-14", Dimension: "charter_runs", Quantity: 1}, } code, body := postUsage(t, ts.URL, agent.Token, rows) if code != http.StatusOK { t.Fatalf("relay: %d %v", code, body) } if gotAuth != "Bearer meter-tok" { t.Fatalf("cloud auth = %q", gotAuth) } if len(gotRows) != 2 || gotRows[0].Quantity != 150 { t.Fatalf("cloud rows = %+v", gotRows) } if budget, _ := body["budget"].(map[string]any); budget["state"] != "low" { t.Fatalf("budget not passed through: %v", body) } } // Unconfigured (self-hosted / BYO key): accept, drop, and say "unmetered". func TestBotUsageUnmetered(t *testing.T) { ts, _ := newTestServer(t) agent := provisionNamed(t, ts.URL, "Eleven", true) code, body := postUsage(t, ts.URL, agent.Token, []botUsageRow{ {Day: "2026-07-14", Dimension: "llm_requests", Quantity: 1}}) if code != http.StatusOK { t.Fatalf("unmetered: %d %v", code, body) } if budget, _ := body["budget"].(map[string]any); budget["state"] != "unmetered" { t.Fatalf("want unmetered, got %v", body) } } // The gate: bots only, and malformed rows never leave the instance. func TestBotUsageGateAndValidation(t *testing.T) { ts, s := newTestServer(t) alice := provisionNamed(t, ts.URL, "Alice", false) agent := provisionNamed(t, ts.URL, "Eleven", true) s.meterURL, s.meterToken = "http://cloud.invalid", "tok" // must never be dialed below good := []botUsageRow{{Day: "2026-07-14", Dimension: "llm_requests", Quantity: 1}} if code, _ := postUsage(t, ts.URL, alice.Token, good); code != http.StatusForbidden { t.Fatalf("member token: want 403, got %d", code) } if code, _ := postUsage(t, ts.URL, "nonsense", good); code != http.StatusForbidden { t.Fatalf("stranger: want 403, got %d", code) } for name, rows := range map[string][]botUsageRow{ "empty": {}, "bad day": {{Day: "yesterday", Dimension: "llm_requests", Quantity: 1}}, "bad dimension": {{Day: "2026-07-14", Dimension: "plaintext_words", Quantity: 1}}, "bad quantity": {{Day: "2026-07-14", Dimension: "llm_requests", Quantity: 0}}, } { if code, _ := postUsage(t, ts.URL, agent.Token, rows); code != http.StatusBadRequest { t.Fatalf("%s: want 400, got %d", name, code) } } // A report without a well-formed dedupe id is refused too. if code, _ := postUsageReport(t, ts.URL, agent.Token, "not hex!", good); code != http.StatusBadRequest { t.Fatal("bad report id: want 400") } } // A cloud blip is a 502 — the sidecar keeps its counters and retries. func TestBotUsageCloudDown(t *testing.T) { ts, s := newTestServer(t) agent := provisionNamed(t, ts.URL, "Eleven", true) dead := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) dead.Close() // connection refused from here on s.meterURL, s.meterToken = dead.URL, "tok" code, _ := postUsage(t, ts.URL, agent.Token, []botUsageRow{ {Day: "2026-07-14", Dimension: "llm_requests", Quantity: 1}}) if code != http.StatusBadGateway { t.Fatalf("cloud down: want 502, got %d", code) } }