summaryrefslogtreecommitdiff
path: root/web/test/budget.test.mjs
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-06-03 23:01:39 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-06-03 23:01:39 +0000
commit68cf1e24dd2ca2612b72babc4b35280cd3512f92 (patch)
tree14b73f21570a2f42ae729ca7e9676de6628d6819 /web/test/budget.test.mjs
parentb28cfc6ff288d083f6c8e9c055b69bfcadbceccc (diff)
parent7388337d3be5cc7f65ef547e30b2e39884dd165b (diff)
merge: integrate oss branch — budget gating, MCP back-channel, events, loopback-only bind
Key features from OSS branch: - Budget gating: rolling per-provider spend caps with BUDGET_EXCEEDED task state - Agent MCP back-channel: runners mint tokens; /mcp endpoint resolves them - Chatbot MCP server at /chatbot/mcp (requires api_token in config) - Event timeline: unified event stream replacing ad-hoc question/summary flows - Loopback-only default bind (127.0.0.1:8484); external_bind_allowed=true to expose - repository_url required on task creation (enforces traceability) - Auto-checker: spawns verification task after each execution Conflict resolutions: - serve.go: keep cfg.Runners.XEnabled() conditional registration from main + agentRegistry from oss - config.go: keep RunnersConfig from main + BudgetConfig/ExternalBindAllowed from oss - server.go: handleStaticFiles (base-path rewrite) kept; deduplicated duplicate from both sides - executor/claude.go: accepted oss deletion (ClaudeRunner replaced by ContainerRunner) - container_test.go: added noopChannel + AgentChannel parameter to Run call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/test/budget.test.mjs')
-rw-r--r--web/test/budget.test.mjs58
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);
+ });
+});