Files
stack/packages/webui/tests/limits-route.test.mjs
T
jason.woltjeandClaude Opus 5.5 aa625565f6 bus, cli, webui: token limits per instance, model, provider and key (row 125, #1619)
Adds token limits under launch.limits.tokens (instances, providers, keys,
models.P.M). Core (packages/bus/src/limits.mjs) reads the organization
file and the per-turn usage records row 124 writes, then asks every
switched-on limit-check plug-in; the first refusal wins. A limit no
switched-on check reports reads "not enforced". The limit-check kind is
interface 2 (mayLaunch, mustStop, report); a v1 check is refused at load.
The shipped token-limit check is pure and counts known end records only;
unknown turns add nothing and are shown as their own count.

The launcher refuses limit-reached through the host's mayLaunch op, or
limit-check-failed naming the check. The broker asks mustStop after a
usage end and stops the run at its next receive or state. Settings stamps
since on a new or changed limit and runs reset; launch.mjs and
business.mjs validate the shape. Readers: mosaic limits and mosaic limits
reset, the bus limits view, the Console /api/bus/limits route and the
Settings token table with Reset. Key limits stay "not enforced" until
row 126 names accounts on records.

Owner Dewey, design under lead decision 150.
Reviewer Filbert: r1 changes (comment 28965), r2 approve (28981), r3
approve (28996) on candidate manifest 8b71c405...fe56 after the rebase
onto row 164 (37 of 38 paths with identical +/- lines, TOOLS.md:621
bracket). Decision 123 go on the bus.js hunk from Mos (mos-0814). Sage
gate on b6827d19e: 25 steps, all rc 0 (15 packages, 10 script suites), 2295 node tests and 422 script selftests, 0 failed, 20:03:25Z to 20:08:08Z, network sage-r125_default removed.

Live stays as pinned. Not part of the 8e7ea89a1 window.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 15:21:49 -05:00

70 lines
3.6 KiB
JavaScript

// Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): the Console's token limit read and the Settings reset.
import test from 'node:test';
import assert from 'node:assert/strict';
import {request} from 'node:http';
import {startServer} from '../src/serve.mjs';
import {close} from './fixture.mjs';
import {views} from '../../bus/src/views.mjs';
const SINCE = '2026-10-18T07:00:00.000Z';
const TOKENS = [
{ path: ['tokens', 'instances', 'coder'], limit: 100, since: SINCE, status: 'counted', check: 'token-limit', count: 120,
kinds: { input: 10, output: 20, cacheWrite: 40, cacheRead: 50 }, unknownTurns: 2 },
{ path: ['tokens', 'keys', 'k1'], limit: 5, since: SINCE, status: 'not-enforced', reason: 'no records name a key' },
{ path: ['tokens', 'providers', 'zai'], limit: 9, since: SINCE, status: 'not-enforced', reason: null },
];
test('REQ-LIMIT-3 limits read returns only the token rows; no query or write is accepted', async t => {
let calls = 0;
const server = await startServer({ port: 0, bus: { limits: async () => { calls++; return { revision: 'a'.repeat(64), limits: { total: 3 }, tokens: TOKENS }; } } });
t.after(() => close(server));
const url = `http://127.0.0.1:${server.address().port}/api/bus/limits`;
const response = await fetch(url), body = await response.json();
assert.equal(response.status, 200);
assert.deepEqual(Object.keys(body).sort(), ['at', 'rows']);
assert.deepEqual(body.rows, TOKENS);
assert.equal((await fetch(url + '?business=other')).status, 400);
assert.equal((await fetch(url, { method: 'POST' })).status, 405);
assert.equal(calls, 1);
});
test('REQ-LIMIT-3 a limits answer without a token list is refused, not shown as no limits', async t => {
for (const answer of [{}, { tokens: null }, null]) {
const server = await startServer({ port: 0, bus: { limits: async () => answer } });
t.after(() => close(server));
assert.equal((await fetch(`http://127.0.0.1:${server.address().port}/api/bus/limits`)).status, 502, JSON.stringify(answer));
}
});
const preview = { host: 'console.example', header: 'x-user', user: 'jason' };
const headers = { host: 'console.example', 'x-user': 'jason', origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' };
function post(port, data) {
return new Promise((resolve, reject) => {
const req = request({ hostname: '127.0.0.1', port, path: '/api/settings', method: 'POST', headers }, r => {
let text = ''; r.on('data', c => text += c); r.on('end', () => resolve({ status: r.statusCode, text }));
});
req.on('error', reject);
req.end(JSON.stringify(data));
});
}
test('REQ-LIMIT-3 a Settings reset reaches the human write with its paths; a reset alone is enough', async t => {
const calls = [];
const server = await startServer({ port: 0, preview, send: async () => ({}), settingsWrite: async args => { calls.push(args); return { ...args, business: 'acme' }; } });
t.after(() => new Promise(r => server.close(r)));
const port = server.address().port;
const reset = { revision: 'a'.repeat(64), reset: [['tokens', 'instances', 'coder']] };
const r = await post(port, reset);
assert.equal(r.status, 200, r.text);
assert.deepEqual(calls, [reset]);
assert.equal((await post(port, { revision: 'a'.repeat(64), resets: [] })).status, 400);
assert.equal(calls.length, 1);
});
test('REQ-LIMIT-3 the Console reader asks the bus limits verb with no arguments', async () => {
const calls = [];
const v = views({ call: async (...args) => (calls.push(args), { tokens: [] }) });
assert.deepEqual(await v.limits(), { tokens: [] });
assert.deepEqual(calls, [['limits']]);
});