153 lines
16 KiB
JavaScript
153 lines
16 KiB
JavaScript
// Row 125 (#1619) mutants: one per check, each an exact-once text edit in one candidate file,
|
|
// each named with the test files that must fail on it. Driven by mutants.sh; run from the worktree.
|
|
// node mutants.mjs list name<TAB>file<TAB>test files (space separated)
|
|
// node mutants.mjs check read only: each old text matches once, each test file exists
|
|
// node mutants.mjs apply NAME apply NAME's edit; exit 3 unless the old text matches exactly once
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
|
const L = 'packages/bus/src/limits.mjs', LT = 'packages/bus/tests/limit-checks.test.mjs';
|
|
const TL = 'packages/plugins/limit-check/token-limit/index.mjs', TT = 'packages/plugins/tests/token-limit.test.mjs';
|
|
const B = 'packages/bus/src/broker.mjs', ST = 'packages/bus/tests/limit-stop.test.mjs', RT = 'packages/harness/tests/runner.test.mjs';
|
|
const LA = 'packages/cli/src/launcher.mjs', LCT = 'packages/cli/tests/launch-limit-checks.test.mjs', THT = 'packages/cli/tests/thread-launch.test.mjs';
|
|
const LS = 'packages/business/src/launch-settings.mjs', TS = 'packages/business/tests/token-limit-settings.test.mjs';
|
|
const LM = 'packages/business/src/launch.mjs', BU = 'packages/business/src/business.mjs';
|
|
const K = 'packages/plugins/src/kinds.mjs', KT = 'packages/plugins/tests/kinds.test.mjs', LOT = 'packages/plugins/tests/loader.test.mjs';
|
|
const SV = 'packages/webui/src/serve.mjs', WRT = 'packages/webui/tests/limits-route.test.mjs';
|
|
const BJ = 'packages/webui/src/public/bus.js', WBT = 'packages/webui/tests/limits-browser.test.mjs';
|
|
const CL = 'packages/cli/src/limits.mjs', CLT = 'packages/cli/tests/limits-tokens.test.mjs';
|
|
const RU = 'packages/bus/src/runtime.mjs', RUT = 'packages/bus/tests/limits-runtime.test.mjs';
|
|
const PR = 'packages/bus/src/process.mjs', VW = 'packages/bus/src/views.mjs';
|
|
const HS = 'packages/harness/src/tools.mjs', HT = 'packages/harness/tests/tools.test.mjs';
|
|
const U = 'packages/bus/src/usage.mjs', UT = 'packages/bus/tests/usage.test.mjs';
|
|
|
|
const PRT = 'packages/bus/tests/process.test.mjs';
|
|
const M = [
|
|
// core: packages/bus/src/limits.mjs
|
|
['L02-decide-skips-failed-check', L, [LT], "if (code) return halt({ check: check.name, code });", "if (code) continue;"],
|
|
['L03-unreadable-state-goes', L, [LT], "return halt({ check: null, code: e.code });", "return { [key]: go };"],
|
|
['L04-hit-any-path', L, [LT], "leaves.some(l => same(l.path, hit.path) && l.limit === hit.limit) && ", ""],
|
|
['L05-reason-dropped', L, [LT], "reason: named ? named.e.notEnforced : null", "reason: null"],
|
|
['L06-report-skips-failed-check', L, [LT], "if (code) return failedAll(check.name, code);", "if (code) continue;"],
|
|
['L07-decide-no-not-loaded', L, [LT],
|
|
" if (!st.checks) {\n log({ event: 'limit-check-failed', business, check: st.missing, export: name, code: 'not-loaded' });\n return halt({ check: st.missing, code: 'not-loaded' });\n }\n", ""],
|
|
['L08-report-no-not-loaded', L, [LT], "if (!st.checks) return failedAll(st.missing, 'not-loaded');", "if (!st.checks) return [];"],
|
|
['L09-answer-not-validated', L, [LT], " if (!valid(answer)) throw new Failure('malformed-answer');\n", ""],
|
|
['L10-own-code-lost', L, [LT], "typeof e?.code === 'string' && CODE.test(e.code) ? e.code : 'check-threw'", "'check-threw'"],
|
|
['L11-entry-limit-unchecked', L, [LT], "if (!leaf || e.limit !== leaf.limit || e.since !== leaf.since) return false;", "if (!leaf) return false;"],
|
|
['L12-reach-not-logged', L, [LT], " log({ event: 'limit-reached', business, check: check.name, export: name, subject, ...answer.hit });\n", ""],
|
|
['L13-decide-no-refresh', L, [LT], " await refresh(business);\n let st;\n", " let st;\n"],
|
|
['L14-subject-account-dropped', L, [LT], "=> ({ instance, provider, model, account });", "=> ({ instance, provider, model });"],
|
|
['L16-leaves-any-depth', L, [LT], "export function limitLeaves(limits) {\n", "export function limitLeaves(limits) { return anyLeaves(limits); }\nfunction anyLeaves(limits, path = []) {\n const out = [];\n if (!plainObject(limits)) return out;\n for (const [key, value] of Object.entries(limits)) {\n if (own(value, 'limit')) out.push({ path: [...path, key], limit: value.limit, since: value.since });\n else if (plainObject(value)) out.push(...anyLeaves(value, [...path, key]));\n }\n return out;\n}\nfunction depthLeaves(limits) {\n"],
|
|
// round 2 (Filbert f3): each check gets its own copy of the args
|
|
['L17-args-shared', L, [LT], "check.module[name](structuredClone(args))", "check.module[name](args)"],
|
|
['L15-thenable-unguarded', L, [LT],
|
|
" if (answer !== null && (typeof answer === 'object' || typeof answer === 'function') && typeof answer.then === 'function') {\n Promise.resolve(answer).catch(() => {});\n throw new Failure('malformed-answer');\n }\n", ""],
|
|
// the shipped check: packages/plugins/limit-check/token-limit/index.mjs
|
|
['T01-over-not-at', TL, [TT, LT], "if (count >= BigInt(leaf.entry.limit))", "if (count > BigInt(leaf.entry.limit))"],
|
|
['T02-since-ignored', TL, [TT, LT], "if (!leaf.match(r) || time(r.at) < since) continue;", "if (!leaf.match(r)) continue;"],
|
|
['T03-unknown-not-counted', TL, [TT], "else unknown.add(JSON.stringify([r.run, r.turn]));", "else void 0;"],
|
|
['T04-unknown-counted-as-known', TL, [TT, LT], "if (r.status === 'known') for", "if (r.status !== null) for"],
|
|
['T05-key-always-enforced', TL, [TT], "if (leaf.key && !named)", "if (false)"],
|
|
['T06-instance-matches-all', TL, [TT], "match: x => x.instance === n });", "match: x => true });"],
|
|
['T07-provider-matches-all', TL, [TT], "match: x => x.provider === p });", "match: x => true });"],
|
|
['T08-model-ignores-model', TL, [TT], "match: x => x.provider === p && x.model === m });", "match: x => x.provider === p });"],
|
|
['T09-key-matches-all', TL, [TT], "match: x => x.account === k, key: true });", "match: x => true, key: true });"],
|
|
['T10-big-counts-lossy', TL, [TT], "const plain = n => n <= SAFE ? Number(n) : String(n);", "const plain = n => Number(n);"],
|
|
['T11-bad-records-pass', TL, [TT], "const amount = value => Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : refuse('bad-record');", "const amount = value => BigInt(value ?? 0);"],
|
|
// round 2 (Filbert's no-source note): an unknown turn adds nothing and stops nothing
|
|
['T12-unknown-turn-stops', TL, [RUT], "if (count >= BigInt(leaf.entry.limit))", "if (count + BigInt(unknownTurns) >= BigInt(leaf.entry.limit))"],
|
|
// broker: the end-of-turn stop
|
|
['B01-throw-goes-on', B, [ST], "answer = { stop: true, check: null, code: 'check-threw' };", "answer = { stop: false };"],
|
|
['B02-malformed-kept', B, [ST], "answer?.stop === true ? answer : { stop: true, check: null, code: 'malformed-answer' }", "answer"],
|
|
['B03-no-stop-on-state', B, [ST], "(request.verb === 'message.receive' || request.verb === 'state')", "request.verb === 'message.receive'"],
|
|
['B04-by-run-not-limit-check', B, [ST, RT], "this.#claimEnd(s, h, s.thread ? 'release' : 'revoke', null, reason, 'limit-check');", "this.#claimEnd(s, h, s.thread ? 'release' : 'revoke', null, reason);"],
|
|
['B05-stop-before-result', B, [ST, RT], "if (phase === 'end') await this.#limitCheck(s);",
|
|
"if (phase === 'end') { await this.#limitCheck(s); if (this.#stops.has(s.business + ':' + s.run)) this.#limitStop(s); }"],
|
|
['B06-endlaunch-no-limit', B, [ST], "return { released, ...(limit ? { limit } : {}) };", "return { released };"],
|
|
['B07-thread-revoked', B, [ST], "this.#claimEnd(s, h, s.thread ? 'release' : 'revoke', null, reason, 'limit-check');", "this.#claimEnd(s, h, 'revoke', null, reason, 'limit-check');"],
|
|
['B08-reason-no-count', B, [ST, RT], " limit ${stop.hit.limit} count ${stop.hit.count}`", " limit ${stop.hit.limit}`"],
|
|
['B09-org-keys-no-reset', B, [HT], "export const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances', 'reset'];", "export const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances'];"],
|
|
// launcher
|
|
['A01-op-failure-allows', LA, [LCT], "throw new Refusal(\"limit-check-failed\", `bus: ${e.code ?? \"launch-op-refused\"}`);", "may = { allow: true };"],
|
|
['A02-only-false-refuses', LA, [LCT], "if (may?.allow !== true) {", "if (may?.allow === false) {"],
|
|
['A03-hit-not-limit-reached', LA, [LCT], "if (may?.hit) { const error", "if (false) { const error"],
|
|
['A04-end-record-no-limit', LA, [THT], ", ...(ended?.limit ? { limit: ended.limit } : {}) });", " });"],
|
|
// business: the writer and the schema
|
|
['S01-since-overwrite-accepted', LS, [TS], " if (own(entry, \"since\") && entry.since !== kept) throw new OrganizationError(\"invalid-request\");\n", ""],
|
|
['S02-change-restamps', LS, [TS], "entry.since = kept ?? now;", "entry.since = now;"],
|
|
['S03-reset-no-move', LS, [TS], " leaf.entry.since = now;\n", ""],
|
|
['S04-reset-any-path', LS, [TS], "const leaf = leaves.find(l => l.path.length === path.length && l.path.every((part, i) => part === path[i]));", "const leaf = { entry: at(limits, path) };"],
|
|
['S05-settings-keys-no-reset', LS, [TS, HT], "export const SETTINGS_KEYS = Object.freeze([\"revision\", \"limits\", \"models\", \"instances\", \"reset\"]);", "export const SETTINGS_KEYS = Object.freeze([\"revision\", \"limits\", \"models\", \"instances\"]);"],
|
|
['S06-reset-alone-refused', LS, [TS], "if (![\"limits\", \"models\", \"instances\", \"reset\"].some(", "if (![\"limits\", \"models\", \"instances\"].some("],
|
|
// round 2 (Filbert change 1): token limits only at schema depth; each mutant puts r1's any-object-with-limit walk back in one place
|
|
['S08-stamp-any-depth', LS, [TS], "for (const { path, entry } of tokenLeaves(next)) {", "for (const { path, entry } of (function any(v, p = []) { return plain(v) ? (own(v, \"limit\") && p.length ? [{ path: p, entry: v }] : Object.entries(v).flatMap(([k, x]) => any(x, [...p, k]))) : []; })(next)) {"],
|
|
['S09-reset-any-depth', LS, [TS], " const leaves = tokenLeaves(limits);\n", " const leaves = (function any(v, p = []) { return plain(v) ? (own(v, \"limit\") && p.length ? [{ path: p, entry: v }] : Object.entries(v).flatMap(([k, x]) => any(x, [...p, k]))) : []; })(limits);\n"],
|
|
['S07-reset-empty-accepted', LS, [TS], "if (!Array.isArray(paths) || !paths.length) throw", "if (!Array.isArray(paths)) throw"],
|
|
['D01-since-no-roundtrip', LM, [TS], "new Date(since).toISOString() !== since) refuse", "false) refuse"],
|
|
['D02-limit-unchecked', LM, [TS], "return { limit: count(value.limit, `${at}.limit`), since };", "return { limit: value.limit, since };"],
|
|
['D03-leaf-extra-keys', LM, [TS], " rejectUnknownKeys(value, [\"limit\", \"since\"], at);\n", ""],
|
|
['D04-token-extra-kinds', LM, [TS], " rejectUnknownKeys(value, [\"instances\", \"providers\", \"models\", \"keys\"], where);\n", ""],
|
|
['D05-instance-undeclared', BU, [TS], " for(const instance of Object.keys(limits.tokens?.instances??{}))if(!Object.hasOwn(roles,instance))refuse(", " for(const instance of [])if(!Object.hasOwn(roles,instance))refuse("],
|
|
// plug-in contract
|
|
['K01-version-1', K, [KT, LOT], "kind: \"limit-check\", interfaceVersion: 2,", "kind: \"limit-check\", interfaceVersion: 1,"],
|
|
['K02-no-report-export', K, [KT, LOT], "exports: Object.freeze([\"mayLaunch\", \"mustStop\", \"report\"])", "exports: Object.freeze([\"mayLaunch\", \"mustStop\"])"],
|
|
// Console server, page and reader
|
|
['W01-no-tokens-check', SV, [WRT], "if (!value || !Array.isArray(value.tokens)) throw new BusReadError('invalid-response');", "if (!value) throw new BusReadError('invalid-response');"],
|
|
['W02-writable-no-reset', SV, [WRT], "const writable = ['limits', 'models', 'instances', 'reset'];", "const writable = ['limits', 'models', 'instances'];"],
|
|
['W03-limits-takes-query', SV, [WRT], " if (search) return json(res, 400, { error: 'invalid-request', message: 'limits takes no arguments' });\n", ""],
|
|
['W04-whole-document-out', SV, [WRT], "return json(res, 200, { rows: value.tokens, at: new Date().toISOString() });", "return json(res, 200, { ...value, rows: value.tokens, at: new Date().toISOString() });"],
|
|
['W05-view-not-limits', VW, [WRT], "limits: () => client.call('limits'),", "limits: () => client.call('usage'),"],
|
|
['W06-page-save-drops-tokens', BJ, [WBT], " if(v.limits?.tokens)limits.tokens=structuredClone(v.limits.tokens);\n", ""],
|
|
['W07-page-reason-dropped', BJ, [WBT], "`not enforced${t.reason ? `: ${txt(t.reason)}` : ''}`", "'not enforced'"],
|
|
['W08-page-reset-ungated', BJ, [WBT], "data-limit-reset=\"${esc(JSON.stringify(t.path || []))}\"${can ? '' : ' disabled'}>", "data-limit-reset=\"${esc(JSON.stringify(t.path || []))}\">"],
|
|
['W09-page-check-unescaped', BJ, [WBT], "`check failed: ${txt(t.code)}${t.check ? ` (${txt(t.check)})` : ''}`", "`check failed: ${txt(t.code)}${t.check ? ` (${t.check})` : ''}`"],
|
|
['W10-page-refused-shows-none', BJ, [WBT], "limitsRefused = true; }", "}"],
|
|
// round 2 (Filbert f4): a reset under a dirty draft keeps the snapshot the draft was typed on
|
|
['W11-reset-replaces-draft-snapshot', BJ, [WBT], "if(!settingsInputs)settingsSnapshot=result;", "settingsSnapshot=result;"],
|
|
// CLI
|
|
['C01-cli-reason-dropped', CL, [CLT], "return `${head}, not enforced${t.reason ? `: ${t.reason}` : \"\"}`;", "return `${head}, not enforced`;"],
|
|
['C02-reset-path-short', CL, [CLT], "reset: [parsed.positional.slice(1)]", "reset: [parsed.positional.slice(2)]"],
|
|
['C03-reset-no-path-sent', CL, [CLT], "(verb === \"reset\" && parsed.positional.length < 2)) {", "false) {"],
|
|
['C04-cli-tokens-in-json', CL, [CLT], "io.stdout.write(JSON.stringify(rest, null, 2)", "io.stdout.write(JSON.stringify(result, null, 2)"],
|
|
// bus runtime and its process
|
|
['R01-no-tokens', RU, [RUT], "const withTokens = (id, result) => ({ ...result, tokens: limits.report(id) });", "const withTokens = (id, result) => result;"],
|
|
['R02-maylaunch-allows', RU, [RUT], "mayLaunch: ({ business, instance, provider, model }) => limits.mayLaunch(business, { instance, provider, model }),", "mayLaunch: async () => ({ allow: true }),"],
|
|
['R03-broker-no-limits', RU, [RUT], "\n limits,\n", "\n"],
|
|
['R04-reset-not-recounted', RU, [RUT], "if(verb==='limits.set')return withTokens(id,settings.set(id,args,actor));", "if(verb==='limits.set')return settings.set(id,args,actor);"],
|
|
// round 2 (Filbert f2): plugins.set reloads the limit checks
|
|
['R05-plugins-set-no-refresh', RU, [RUT], " limits.refresh(id).catch(e=>limitLog({event:'limit-refresh-failed',business:id,message:e.message}));\n", ""],
|
|
['P01-op-not-awaited', PR, [PRT], "result: await launchOp(message)", "result: launchOp(message)"],
|
|
['P02-op-missing', PR, [PRT], " if (m.op === 'mayLaunch') return runtime.mayLaunch(m);\n", ""],
|
|
// harness tool
|
|
['H01-tool-keys-no-reset', HS, [HT], "export const ORGANIZATION_KEYS = Object.freeze([\"revision\", \"limits\", \"models\", \"instances\", \"reset\"]);", "export const ORGANIZATION_KEYS = Object.freeze([\"revision\", \"limits\", \"models\", \"instances\"]);"],
|
|
// usage records the checks count
|
|
['U01-begin-row-known', U, [UT, LT], "const known = row.phase === 'end' && row.status === 'known';", "const known = row.status === 'known';"],
|
|
];
|
|
|
|
const [verb, name] = process.argv.slice(2);
|
|
if (verb === 'list') {
|
|
const seen = new Set();
|
|
for (const [n, file, tests] of M) {
|
|
if (seen.has(n)) { console.error(`duplicate ${n}`); process.exit(2); }
|
|
seen.add(n);
|
|
console.log(`${n}\t${file}\t${tests.join(' ')}`);
|
|
}
|
|
} else if (verb === 'check') {
|
|
// Read only: every mutant's old text must match exactly once, and every named test file must exist.
|
|
let bad = 0;
|
|
for (const [n, file, tests, before] of M) {
|
|
const count = readFileSync(file, 'utf8').split(before).length - 1;
|
|
if (count !== 1) { bad++; console.log(`${n}\t${file}\tmatches ${count}`); }
|
|
for (const t of tests) try { readFileSync(t); } catch { bad++; console.log(`${n}\tmissing test ${t}`); }
|
|
}
|
|
console.log(`${M.length} mutants, ${bad} problems`);
|
|
process.exit(bad ? 1 : 0);
|
|
} else if (verb === 'apply') {
|
|
const m = M.find(x => x[0] === name);
|
|
if (!m) { console.error(`no mutant ${name}`); process.exit(2); }
|
|
const [, file, , before, after] = m;
|
|
const text = readFileSync(file, 'utf8'), n = text.split(before).length - 1;
|
|
if (n !== 1) { console.error(`${name}: expected one match in ${file}, found ${n}`); process.exit(3); }
|
|
writeFileSync(file, text.replace(before, () => after));
|
|
} else { console.error('usage: mutants.mjs list | apply NAME'); process.exit(2); }
|