#!/usr/bin/env python3 # Row 294 mutants M1-M11 (BUILD.md "Mutants"). Each mutant edits the worktree (every old text must # match exactly once), runs its killing file alone under timeout -k 10 300 with the TAP reporter, # then restores with git checkout and checks the tree is clean. Killed = the named killing test(s) # report "not ok". M1 also needs G5 "ok" in the same run (Duckworth C1). import json, os, re, subprocess, sys, time WT = '/mnt/storage/scratch/launchpad-r294/wt' OUT = '/mnt/storage/scratch/launchpad-r294/run2' GU = ('packages/webui', 'tests/granted-user.test.mjs') AB = ('packages/cli', 'tests/access-boot.test.mjs') CHAT_CALL = "verb === 'plugins' ? owner('plugins', {}) : call(verb, args)" MUTANTS = [ ('M1', [('packages/webui/src/serve.mjs', "if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only'", "if (false) return json(res, 403, { error: 'owner-only'")], GU, ['R6 G4'], ['R6 G5']), ('M2', [('packages/webui/src/sso.mjs', "if (s.user !== undefined && !users?.check(s)) {", "if (s.user !== undefined && false) {")], GU, ['R6 G3'], []), ('M3', [('packages/webui/src/public/user.html', "but not your messages.", "but not your replies.")], GU, ['R6 G6'], []), ('M4', [('packages/cli/src/access.mjs', "const peers = (system.federation?.peers ?? []).map((p) => p.id);", "const peers = [];")], AB, ['R6 a configured peer id', 'R6 B1'], []), ('M5', [('packages/webui/src/access.mjs', "return user && live(doc, user.id, b) ? user.id : null;", "return user ? user.id : null;")], GU, ['R6 G2'], []), ('M6', [('packages/webui/src/user-bus.mjs', "env: { ...rest, MOSAIC_BUS_USER_NONCE", "env: { ...env, MOSAIC_BUS_USER_NONCE")], GU, ['R6 G8'], []), ('M7', [('packages/cli/src/config.mjs', " if (access) config.access = access;\n", "")], AB, ['R6 B1'], []), ('M8', [('packages/webui/src/chat.mjs', CHAT_CALL, "verb === 'plugins' ? owner('plugins', {}) : verb === 'thread.list' ? owner(verb, args) : call(verb, args)")], GU, ['R6 G9'], []), ('M9', [('packages/cli/src/config.mjs', " warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);\n", "")], AB, ['R6 B3'], []), ('M10', [('packages/webui/src/console-bus.mjs', "return session => consoleBus(transport(session.user), { send, owner });", "return session => consoleBus(transport(session.user), { send, owner: (v, a) => owner(v, { ...a, sub: session.sub }) });")], GU, ['R6 G9'], []), ('M11', [('packages/webui/src/chat.mjs', "const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => " + CHAT_CALL + " });", "let last = {};\n const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', last) : call(verb, args) });"), ('packages/webui/src/chat.mjs', " try { return await dispatch(verb, args); }", " try { last = args; return await dispatch(verb, args); }")], GU, ['R6 G9'], []), ] def git(*a): return subprocess.run(['git', '-C', WT, *a], capture_output=True, text=True, check=True).stdout def restore(files): if files: git('checkout', '--', *sorted(files)) if git('status', '--porcelain'): raise SystemExit('worktree not clean after restore') def results(tap): # Top-level TAP test lines: "ok N - name" / "not ok N - name" at no indentation. return {m.group(2): m.group(1) == 'ok' for m in re.finditer(r'^(ok|not ok) \d+ - (.*)$', tap, re.M)} def main(): only = sys.argv[1:] if git('status', '--porcelain'): raise SystemExit('worktree not clean at start') env = dict(os.environ) summary, bad = [], 0 for mid, edits, (pkg, test), killers, greens in MUTANTS: if only and mid not in only: continue files = set() try: for path, old, new in edits: p = os.path.join(WT, path); s = open(p).read() if s.count(old) != 1: raise SystemExit(f'{mid}: anchor count {s.count(old)} in {path}') open(p, 'w').write(s.replace(old, new)); files.add(path) diff = git('diff') t0 = time.time() r = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap', test], cwd=os.path.join(WT, pkg), capture_output=True, text=True, env=env) dt = round(time.time() - t0, 1) finally: restore(files) open(f'{OUT}/{mid}.diff', 'w').write(diff) open(f'{OUT}/{mid}.tap', 'w').write(r.stdout + '\n# stderr\n' + r.stderr) res = results(r.stdout) hit = lambda pre: [n for n, ok in res.items() if n.startswith(pre)] red = [n for n, ok in res.items() if not ok] killed = r.returncode not in (0, 124, 137) and all(any(not res[n] for n in hit(k)) for k in killers) \ and all(hit(g) and all(res[n] for n in hit(g)) for g in greens) bad += not killed line = f'{mid} rc={r.returncode} seconds={dt} killed={killed} red={red}' print(line, flush=True) summary.append({'mutant': mid, 'rc': r.returncode, 'seconds': dt, 'killed': killed, 'red': red, 'killers': killers, 'greens': greens}) json.dump(summary, open(f'{OUT}/mutants.json', 'w'), indent=1) sys.exit(1 if bad else 0) main()