diff options
| author | Claude <noreply@anthropic.com> | 2026-05-26 20:34:09 +0000 |
|---|---|---|
| committer | Claude <noreply@anthropic.com> | 2026-05-26 20:34:09 +0000 |
| commit | ab4b364954af08fa602388495ca425eaef0abf74 (patch) | |
| tree | c0806a57295270b4d86f097c3d0b6a0e1d631201 /web/test | |
| parent | 32715355fe2eed321df4f7083dfe580d35f8a62a (diff) | |
feat(api,web): budget headroom endpoint + UI chips (Phase 6)
Adds GET /api/budget returning per-provider rolling-window headroom (empty when
budget gating is unconfigured), wired from serve.go via SetBudget. The web UI
polls it and renders a chip per limited provider in the header, flagging any
under 20% remaining. New pure JS helpers formatBudgetHeadroom/
renderBudgetHeadroom are unit-tested; the endpoint is covered by Go handler
tests (empty/reports/500). UI render not browser-verified in this environment.
Completes Phase 6: spend accounting + dispatcher gating + observability surface.
https://claude.ai/code/session_01SESwn7kQ7oP62trWw6pc39
Diffstat (limited to 'web/test')
| -rw-r--r-- | web/test/budget.test.mjs | 58 |
1 files changed, 58 insertions, 0 deletions
diff --git a/web/test/budget.test.mjs b/web/test/budget.test.mjs new file mode 100644 index 0000000..35d5256 --- /dev/null +++ b/web/test/budget.test.mjs @@ -0,0 +1,58 @@ +// budget.test.mjs — Unit tests for budget headroom display. +// +// Run with: node --test web/test/budget.test.mjs + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { formatBudgetHeadroom, renderBudgetHeadroom } from '../app.js'; + +function makeDoc() { + return { + createElement(tag) { + return { + tag, + className: '', + textContent: '', + children: [], + appendChild(child) { this.children.push(child); return child; }, + }; + }, + }; +} + +describe('formatBudgetHeadroom', () => { + it('formats a limited provider with percent and dollars', () => { + assert.equal( + formatBudgetHeadroom({ provider: 'claude', limited: true, limit_usd: 10, remaining_usd: 6, fraction_remaining: 0.6 }), + 'Claude: 60% left ($6.00 of $10.00)', + ); + }); + + it('returns empty string for unlimited providers', () => { + assert.equal(formatBudgetHeadroom({ provider: 'local', limited: false }), ''); + assert.equal(formatBudgetHeadroom(null), ''); + }); +}); + +describe('renderBudgetHeadroom', () => { + it('renders one chip per limited provider, skipping unlimited', () => { + const wrap = renderBudgetHeadroom([ + { provider: 'claude', limited: true, limit_usd: 10, remaining_usd: 6, fraction_remaining: 0.6 }, + { provider: 'local', limited: false }, + ], makeDoc()); + assert.equal(wrap.children.length, 1); + assert.equal(wrap.children[0].className, 'budget-chip'); + assert.match(wrap.children[0].textContent, /Claude: 60% left/); + }); + + it('flags providers under 20% remaining with --low', () => { + const wrap = renderBudgetHeadroom([ + { provider: 'gemini', limited: true, limit_usd: 5, remaining_usd: 0.5, fraction_remaining: 0.1 }, + ], makeDoc()); + assert.equal(wrap.children[0].className, 'budget-chip budget-chip--low'); + }); + + it('returns null when doc is null', () => { + assert.equal(renderBudgetHeadroom([], null), null); + }); +}); |
