// Filbert's row 57 mutants: each edits one file in the mutant worktree, runs // the bus and webui suites, and restores the file. Usage: node run.mjs import { readFileSync, writeFileSync, appendFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; const W = process.argv[2]; const OUT = new URL('.', import.meta.url).pathname; const B = 'packages/bus/src/broker.mjs'; const S = 'packages/webui/src/serve.mjs'; const V = 'packages/bus/src/views.mjs'; const mutants = [ ['role-allowed', B, "if (!s.human && !s.reader) fail('human-required');", ''], ['roles-with-human', B, "AND route_to<>'human' AND", 'AND'], ['roles-with-closed', B, ` AND NOT EXISTS(SELECT 1 FROM decision_events x WHERE x.decision=d.id AND x.op IN \${CLOSED}) ORDER BY seq`, ' ORDER BY seq'], ['roles-other-business', B, "WHERE business=? AND route_to<>'human'", "WHERE ? IS NOT NULL AND route_to<>'human'"], ['recent-other-business', B, 'WHERE d.business=? ORDER BY x.seq DESC', 'WHERE ? IS NOT NULL ORDER BY x.seq DESC'], ['recent-oldest-first', B, 'ORDER BY x.seq DESC LIMIT', 'ORDER BY x.seq ASC LIMIT'], ['recent-by-raise', B, 'ORDER BY x.seq DESC LIMIT', 'ORDER BY d.seq DESC LIMIT'], ['recent-no-limit', B, ' LIMIT ${RECENT}`', '`'], ['recent-limit-plus-one', B, ' LIMIT ${RECENT}`', ' LIMIT ${RECENT + 1}`'], ['recent-resolved-only', B, `JOIN decision_events x ON x.decision=d.id AND x.op IN \${CLOSED}`, `JOIN decision_events x ON x.decision=d.id AND x.op IN ('resolved')`], ['recent-no-closed', B, '({ ...this.#decisionView(d), closed: this.#closed(d.id) })', '({ ...this.#decisionView(d) })'], ['loose-keys', B, "keys(a, ['list'], ['list']);\n if (a.list === 'roles')", "keys(a, ['list', 'limit'], ['list']);\n if (a.list === 'roles')"], ['unknown-list-empty', B, " .map((d) => ({ ...this.#decisionView(d), closed: this.#closed(d.id) }));\n fail('invalid-request');", " .map((d) => ({ ...this.#decisionView(d), closed: this.#closed(d.id) }));\n return [];"], ['reader-refused', B, "'trail', 'decisions'].includes(request.verb))", "'trail'].includes(request.verb))"], ['web-no-list-check', S, "if (!DECISION_LISTS.includes(arg)) return", 'if (false) return'], ['web-no-verb', S, "const BUS_VERBS = ['inbox', 'tasks', 'agents', 'trail', 'decisions'];", "const BUS_VERBS = ['inbox', 'tasks', 'agents', 'trail'];"], ['views-fixed-list', V, "decisions: (list) => client.call('decisions', { list }),", "decisions: () => client.call('decisions', { list: 'roles' }),"], ]; const suites = [ ['bus', ['--test', 'packages/bus/tests/*.test.mjs'], W], ['webui', ['--test', 'tests/'], join(W, 'packages/webui')], ]; const env = { ...process.env, TMPDIR: process.env.HOME + '/filbert-scratch/tmp', DOCKER_HOST: 'unix:///nonexistent.sock' }; const log = join(OUT, process.argv[4] || 'result.txt'); writeFileSync(log, ''); const only = process.argv[3] ? process.argv[3].split(',') : null; for (const [name, file, from, to] of mutants) { if (only && !only.includes(name)) continue; const path = join(W, file); const orig = readFileSync(path, 'utf8'); if (orig.split(from).length !== 2) { appendFileSync(log, `${name}: PATTERN NOT UNIQUE\n`); continue; } writeFileSync(path, orig.replace(from, to)); const parts = []; try { for (const [label, args, cwd] of suites) { const r = spawnSync('node', args, { cwd, env, encoding: 'utf8', timeout: 600000 }); const out = r.stdout + r.stderr; writeFileSync(join(OUT, `${name}.${label}.log`), out); const fail = (out.match(/^ℹ fail (\d+)/m) || [])[1]; const fails = [...out.matchAll(/^\s*✖ (.+?) \(\d/gm)].map((m) => m[1]).filter((t) => !/^tests?\//.test(t)); parts.push(`${label} rc=${r.status} fail=${fail} ${[...new Set(fails)].slice(0, 4).join(' | ')}`); } } finally { writeFileSync(path, orig); } appendFileSync(log, `${name}: ${parts.join(' ; ')}\n`); } appendFileSync(log, 'done\n');