85 lines
7.5 KiB
JavaScript
85 lines
7.5 KiB
JavaScript
// Row 156 (#1651) 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 B = 'packages/bus/src/broker.mjs', LT = 'packages/bus/tests/land.test.mjs';
|
|
const PR = 'packages/bus/src/process.mjs', RU = 'packages/bus/src/runtime.mjs';
|
|
const LA = 'packages/cli/src/launcher.mjs', LCT = 'packages/cli/tests/launcher.test.mjs';
|
|
const RN = 'packages/harness/src/runner.mjs', RT = 'packages/harness/tests/runner.test.mjs';
|
|
const TLT = 'packages/cli/tests/thread-launch.test.mjs';
|
|
|
|
const M = [
|
|
// broker: landHolders, landRelease, usage for a landing turn
|
|
['B01-threaded-listed', B, [LT], " if (role === null && this.#threaded(business, d.raised_by_role)) continue;\n", ""],
|
|
['B03-task-ref-is-decision', B, [LT], " task_ref: context.target,\n", " task_ref: d.id,\n"],
|
|
['B04-unread-released', B, [LT], " if (unread) return { released: false, unread };\n", ""],
|
|
['B05-decision-not-checked', B, [LT], " if (!this.landHolders(record.business).some((x) => x.instance === session.role && x.decision === record.decision))\n fail('decision-not-held');\n", ""],
|
|
['B06-any-run-of-holder', B, [LT], " if (h?.op !== 'claim' || h.holder_run !== record.run) fail('not-holder');\n if (!this.landHolders(", " if (h?.op !== 'claim') fail('not-holder');\n if (!this.landHolders("],
|
|
['B08-ended-run-released', B, [LT], " if (this.#ended.has(key)) fail('run-ended');\n if (session.thread) fail('thread-not-allowed');\n", " if (session.thread) fail('thread-not-allowed');\n"],
|
|
['B09-release-by-run', B, [LT], "this.#claimEnd(session, h, 'release', null, null, 'land-relaunch')", "this.#claimEnd(session, h, 'release', null, null)"],
|
|
['B10-begin-any-turn', B, [LT], " ? this.landHolders(s.business).some((h) => h.instance === s.role && h.decision === args.turn)\n", " ? true\n"],
|
|
['B11-end-no-begin', B, [LT], " : this.#store.get(\"SELECT 1 FROM usage_records WHERE business=? AND run=? AND turn=? AND phase='begin'\", s.business, s.run, args.turn)))\n", " : true))\n"],
|
|
['B12-end-other-run-begin', B, [LT], "WHERE business=? AND run=? AND turn=? AND phase='begin'\", s.business, s.run, args.turn", "WHERE business=? AND turn=? AND phase='begin'\", s.business, args.turn"],
|
|
// round 2 (29033 F1): a run in its landing turns, not yet at message.receive, isn't released
|
|
['B13-landing-not-kept', B, [LT, RT], " if (!this.#received.has(key)) return { released: false, landing: true };\n", ""],
|
|
['B14-receive-not-noted', B, [LT, RT], " this.#received.add(s.business + ':' + s.run);\n", ""],
|
|
// launch ops
|
|
['P01-no-landRelease-op', PR, [LT, LCT], " if (m.op === 'landRelease') return runtime.landRelease(m.record);\n", ""],
|
|
['P02-no-landHolders-op', PR, [LT, LCT], " if (m.op === 'landHolders') return runtime.landHolders(m.business);\n", ""],
|
|
['RU01-no-runtime-landHolders', RU, [LCT], " landHolders: (business) => broker.landHolders(business),\n", ""],
|
|
// launcher: start() and landPass()
|
|
['LA01-no-landPass', LA, [LCT], " await landPass();\n", ""],
|
|
['LA02-unlisted-started', LA, [LCT], " if (closed) return;\n if (!b.launch.instances.includes(h.instance)) continue;\n const s = registry.running(b.id, h.instance, null);\n", " if (closed) return;\n const s = registry.running(b.id, h.instance, null);\n"],
|
|
['LA03-started-again', LA, [LCT], " if (landStarted.has(h.decision)) continue;\n", ""],
|
|
['LA04-start-not-noted', LA, [LCT], " for (const h of holding ?? []) landStarted.add(h.decision);\n", ""],
|
|
['LA05-own-landing-released', LA, [LCT], "if (stopping.has(s.run) || landingOf.get(s.run)?.has(h.decision)) continue;", "if (stopping.has(s.run)) continue;"],
|
|
['LA06-unreleased-stopped', LA, [LCT], " if (!r?.released) continue;\n", ""],
|
|
['LA07-no-sigterm', LA, [LCT], "decision: h.decision });\n try {\n if (startTimeOf(s.runnerPid) === s.runnerStartTime) process.kill(s.runnerPid, \"SIGTERM\");", "decision: h.decision });\n try {\n if (false) process.kill(s.runnerPid, \"SIGTERM\");"],
|
|
['LA08-no-relaunch-record', LA, [LCT], " record({ event: \"land-relaunch\", instance: h.instance, run: s.run, decision: h.decision });\n", ""],
|
|
['LA09-release-refusal-every-tick', LA, [LCT], "if (refusedRelease.get(s.run) !== code) log(", "if (true) log("],
|
|
['LA10-no-landing-written', LA, [LCT], " ...(holding?.length ? { landing: holding.map(({ decision, task_ref }) => ({ decision, task_ref })) } : {}),\n", ""],
|
|
['LA11-land-separate-read', LA, [LCT], ": holding.length > 0;", ": await host.op({ op: \"landHeld\", record: { business: b.id, role: instance, thread } });"],
|
|
['LA13-start-refusal-every-tick', LA, [LCT], " if (refusedLand.get(h.decision) === code) return;\n", ""],
|
|
// round 2 (29033 F2): a thread launch never asks landHolders
|
|
['LA14-thread-asks-landHolders', LA, [TLT], "const holders = thread ? null : await host.op(", "const holders = await host.op("],
|
|
// runner: landing entries and turns
|
|
['RN01-unchecked', RN, [RT], " if (!landingOk(session.landing)) {", " if (false) {"],
|
|
['RN02-extra-key', RN, [RT], "return keys.length === 2 && keys[0]", "return keys[0]"],
|
|
['RN03-no-length-limit', RN, [RT], " && e.decision.length <= 160", ""],
|
|
['RN04-loose-task-ref', RN, [RT], "const LANDING_TASK_REF = /^vikunja:[1-9][0-9]*\\/[1-9][0-9]*$/;", "const LANDING_TASK_REF = /^vikunja:/;"],
|
|
['RN05-no-landing-turns', RN, [RT], " for (const e of session.landing ?? []) {", " for (const e of []) {"],
|
|
['RN06-no-state-block', RN, [RT], "request: `${block}\\n${landingRequest(e)}`", "request: landingRequest(e)"],
|
|
['RN07a-no-landing-begin', RN, [RT], " try { await client.call(\"usage.begin\", { turn: e.decision }); }\n", " try { }\n"],
|
|
['RN07b-no-landing-end', RN, [RT], " try { await client.call(\"usage.end\", { turn: e.decision }); }\n", " try { }\n"],
|
|
];
|
|
|
|
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); }
|