| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "reasonix/internal/event" |
| 8 | "reasonix/internal/provider" |
| 9 | ) |
| 10 | |
| 11 | // renderUsage drives a Usage event through a fresh TextSink (no renderer) and |
| 12 | // returns what it wrote — the usage line, exercised through the event path. |
| 13 | func renderUsage(u *provider.Usage, p *provider.Pricing, d ...*event.CacheDiagnostics) string { |
| 14 | var b strings.Builder |
| 15 | var diag *event.CacheDiagnostics |
| 16 | if len(d) > 0 { |
| 17 | diag = d[0] |
| 18 | } |
| 19 | NewTextSink(&b, nil, 80).Emit(event.Event{Kind: event.Usage, Usage: u, Pricing: p, CacheDiagnostics: diag}) |
| 20 | return b.String() |
| 21 | } |
| 22 | |
| 23 | func TestUsageLine(t *testing.T) { |
| 24 | u := &provider.Usage{ |
| 25 | PromptTokens: 1000, |
| 26 | CompletionTokens: 200, |
| 27 | TotalTokens: 1200, |
| 28 | CacheHitTokens: 900, |
| 29 | CacheMissTokens: 100, |
| 30 | } |
| 31 | |
| 32 | if out := renderUsage(u, nil); !strings.Contains(out, "1200 tok") || !strings.Contains(out, "900 cached / 100 new") { |
| 33 | t.Errorf("usage line = %q (want 1200 tok and 900 cached / 100 new)", out) |
| 34 | } |
| 35 | |
| 36 | // With pricing: 900*0.02 + 100*1 + 200*2 = 518 per 1M = 0.000518 -> "¥0.0005". |
| 37 | if out := renderUsage(u, &provider.Pricing{CacheHit: 0.02, Input: 1, Output: 2, Currency: "¥"}); !strings.Contains(out, "¥0.0005") { |
| 38 | t.Errorf("cost line = %q (want ¥0.0005...)", out) |
| 39 | } |
| 40 | |
| 41 | // nil or zero usage prints nothing. |
| 42 | if out := renderUsage(nil, nil) + renderUsage(&provider.Usage{}, nil); out != "" { |
| 43 | t.Errorf("nil/zero usage should print nothing, got %q", out) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | // TestUsageLineDerivesMissFromHit covers the OpenAI/MiMo shape where only the |
| 48 | // cached count is reported; the displayed "new" value comes from |
| 49 | // PromptTokens - CacheHitTokens. Verifies the absolute split doesn't show 0. |
| 50 | func TestUsageLineDerivesMissFromHit(t *testing.T) { |
| 51 | u := &provider.Usage{ |
| 52 | PromptTokens: 3540, |
| 53 | CompletionTokens: 378, |
| 54 | TotalTokens: 3918, |
| 55 | CacheHitTokens: 1133, |
| 56 | // CacheMissTokens deliberately 0 — provider only reported the hit |
| 57 | } |
| 58 | if out := renderUsage(u, nil); !strings.Contains(out, "1133 cached / 2407 new") { |
| 59 | t.Errorf("usage line = %q (want 1133 cached / 2407 new)", out) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | func TestUsageLineReportsPrefixChurn(t *testing.T) { |
| 64 | u := &provider.Usage{PromptTokens: 100, CompletionTokens: 10, TotalTokens: 110} |
| 65 | d := &event.CacheDiagnostics{ |
| 66 | PrefixChanged: true, |
| 67 | PrefixChangeReasons: []string{"tools", "log_rewrite"}, |
| 68 | } |
| 69 | if out := renderUsage(u, nil, d); !strings.Contains(out, "cache prefix changed: tools+log_rewrite") { |
| 70 | t.Errorf("usage line = %q (want cache prefix change reason)", out) |
| 71 | } |
| 72 | } |
| 73 |