eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "net/http"
5 "testing"
6 "time"
7)
8
9func testRates() map[string]aiRates {
10 return map[string]aiRates{
11 // Round numbers so the arithmetic below reads at a glance:
12 // €10/M in, €50/M out, €12.50/M cache-write, €1/M cache-read.
13 "claude-sonnet-5": {In: 10, Out: 50, CacheWrite: 12.5, CacheRead: 1},
14 }
15}
16
17func TestParseAIRates(t *testing.T) {
18 rates, err := parseAIRates(`{"claude-sonnet-5":{"in":3,"out":15,"cw":3.75,"cr":0.3}}`)
19 if err != nil || rates["claude-sonnet-5"].Out != 15 {
20 t.Fatalf("parse: %v %+v", err, rates)
21 }
22 if r, err := parseAIRates(""); err != nil || r != nil {
23 t.Fatalf("empty must be off: %v %v", r, err)
24 }
25 if _, err := parseAIRates(`{"m":{"in":-1}}`); err == nil {
26 t.Fatal("negative rates must be refused")
27 }
28 if _, err := parseAIRates(`nonsense`); err == nil {
29 t.Fatal("malformed JSON must be refused")
30 }
31}
32
33func TestUsageCostEUR(t *testing.T) {
34 rows := []botUsageRow{
35 {Model: "claude-sonnet-5", Dimension: "llm_input_tokens", Quantity: 1_000_000}, // €10
36 {Model: "claude-sonnet-5", Dimension: "llm_output_tokens", Quantity: 200_000}, // €10
37 {Model: "claude-sonnet-5", Dimension: "llm_cache_read_tokens", Quantity: 3_000_000}, // €3
38 {Model: "claude-sonnet-5", Dimension: "llm_requests", Quantity: 42}, // request counts are free
39 {Dimension: "charter_runs", Quantity: 7}, // activity, not money
40 {Model: "unpriced-model", Dimension: "llm_output_tokens", Quantity: 1_000_000}, // no rate → 0 + a log line
41 }
42 if got := usageCostEUR(rows, testRates()); got != 23 {
43 t.Fatalf("cost = %v, want 23", got)
44 }
45}
46
47// The state machine end to end through the ingest handler: under allowance is
48// ok, over allowance is exhausted, and a drained fleet pool exhausts a space
49// that is itself under its allowance.
50func TestBudgetStateThroughIngest(t *testing.T) {
51 db := openTestCloudDB(t)
52 a := insertSpaceRow(t, db, "a.11ch.at", 1000)
53 b := insertSpaceRow(t, db, "b.11ch.at", 1000)
54 for id, tok := range map[int64]string{a: "tok-a", b: "tok-b"} {
55 if err := db.setMeterTokenHash(id, hashToken(tok)); err != nil {
56 t.Fatal(err)
57 }
58 }
59 s := &cloudServer{db: db, aiRates: testRates(), aiAllowanceEUR: 5, aiPoolEUR: 12}
60 day := time.Now().UTC().Format("2006-01-02")
61
62 report := func(tok, reportID string, outTokens int64) map[string]any {
63 t.Helper()
64 code, body := ingestUsageReport(t, s, tok, reportID, []botUsageRow{
65 {Day: day, Model: "claude-sonnet-5", Dimension: "llm_output_tokens", Quantity: outTokens}})
66 if code != http.StatusOK {
67 t.Fatalf("ingest: %d %v", code, body)
68 }
69 return body["budget"].(map[string]any)
70 }
71
72 // €2 of output: comfortably inside the €5 allowance.
73 if st := report("tok-a", "aaaaaaaaaaaaaaa1", 40_000); st["state"] != "ok" {
74 t.Fatalf("under allowance: %v", st)
75 }
76 // Another €4 → €6 total: over the per-space allowance.
77 if st := report("tok-a", "aaaaaaaaaaaaaaa2", 80_000); st["state"] != "exhausted" {
78 t.Fatalf("over allowance: %v", st)
79 }
80 // Space B is fresh (€4 < €5 allowance)… but the fleet pool (€12) is now
81 // €6 + €4 = €10; one more €4 pushes past it and B exhausts too.
82 if st := report("tok-b", "bbbbbbbbbbbbbbb1", 80_000); st["state"] != "ok" {
83 t.Fatalf("pool not yet drained: %v", st)
84 }
85 if st := report("tok-b", "bbbbbbbbbbbbbbb2", 80_000); st["state"] != "exhausted" {
86 t.Fatalf("pool drained must exhaust: %v", st)
87 }
88
89 // Unconfigured stays measure-only whatever the ledger says.
90 s2 := &cloudServer{db: db}
91 if st := s2.budgetState(a); st != "ok" {
92 t.Fatalf("unconfigured: %q", st)
93 }
94}
95