// Row 170 (#1667) mutants, the brief's two: each an exact-once text edit in one candidate file, // each named with the test files to run and the test titles that must fail on it. Driven by // mutants.sh, run from the worktree. // node mutants.mjs list | check | apply NAME | expect NAME LOG import { readFileSync, writeFileSync } from 'node:fs'; const B = 'packages/bus/src/broker.mjs', BT = 'packages/bus/tests/tester.test.mjs'; const C = 'packages/cli/src/cli.mjs', CT = 'packages/cli/tests/tester.test.mjs'; const ITEM1 = 'a tester message to a role no run holds refuses tester-target-not-running and stores nothing'; const ITEM2 = 'a tester message is refused while the run is only launched, and routes once it claims'; const ITEM4 = 'talk --tester to a stopped role exits 3, prints no sent line and stores nothing'; const M = [ ['M1-bus-no-tester-check', B, [BT, CT], [ITEM1, ITEM2, ITEM4], " if (s.tester && !this.#recipient(s.business, to, thread)) fail('tester-target-not-running');\n", ""], ['M2-cli-no-tester-branch', C, [CT], [ITEM4], " if (e.code === \"tester-target-not-running\" && parsed.flags.has(\"--tester\")) {\n throw new CliError(`${to} isn't running, so the tester's message was refused and nothing was stored`, 3);\n }\n", ""], ]; const [verb, name, log] = 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, every named test file must exist // and every named title must be one of its test files' titles. let bad = 0; for (const [n, file, tests, titles, before] of M) { const count = readFileSync(file, 'utf8').split(before).length - 1; if (count !== 1) { bad++; console.log(`${n}\t${file}\tmatches ${count}`); } let text = ''; for (const t of tests) try { text += readFileSync(t, 'utf8'); } catch { bad++; console.log(`${n}\tmissing test ${t}`); } for (const title of titles) if (!text.includes(JSON.stringify(title)) && !text.includes(`'${title}'`)) { bad++; console.log(`${n}\tno test titled ${title}`); } } 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 if (verb === 'expect') { // The spec reporter marks a failed test "✖ (<ms>)". Every named title must be among them. const m = M.find(x => x[0] === name); if (!m) { console.error(`no mutant ${name}`); process.exit(2); } const failed = new Set([...readFileSync(log, 'utf8').matchAll(/^\s*✖ (.*) \([\d.]+ms\)$/gm)].map(x => x[1])); const missing = m[3].filter(t => !failed.has(t)); for (const t of failed) console.log(`${name}\tfailed\t${t}`); for (const t of missing) console.log(`${name}\tDID NOT FAIL\t${t}`); process.exit(missing.length ? 1 : 0); } else { console.error('usage: mutants.mjs list | check | apply NAME | expect NAME LOG'); process.exit(2); }