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 onb6827d19e: 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 the8e7ea89a1window. Co-Authored-By: Claude Opus 5.5 <[email protected]>
70 lines
3.6 KiB
JavaScript
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']]);
|
|
});
|