// 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 namefiletest 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); }