#!/usr/bin/env python3 """Run only on Sage CLEAR. Mutate isolated HEAD archives; never edit the checkout. A green control run is required before each negative result counts as a kill. Usage: python3 agents/beakley/work/queue-217/mutants.py OUTPUT_DIRECTORY """ from pathlib import Path import io import hashlib import os import subprocess import sys import tarfile import tempfile ROOT = Path(__file__).resolve().parents[4] OUT = Path(sys.argv[1]).resolve() OUT.mkdir(parents=True, exist_ok=True) WRITER = "packages/business/src/organization.mjs" BROKER = "packages/bus/src/broker.mjs" CLIENT = "packages/bus/src/client.mjs" ATOMIC = "if (check) this.#event(s, 'action.allowed', { action: 'message.checked', message: mid, check }, mid, checkEventId(mid));" REENTER = "return this.#reenter(cap, s, { verb: 'message.send', args }, check);" SPLIT = """const result = this.#reenter(cap, s, { verb: 'message.send', args }, check); if (check) this.#store.transaction(() => this.#event(s, 'action.allowed', { action: 'message.checked', message: result.id, check }, result.id, checkEventId(result.id))); return result;""" MUTANTS = [ ("M1-writer-fence", [(WRITER, " replyLimitsFence(old, document, actor);", ""), (WRITER, " replyLimitsFence(old, written, actor);", "")], ["packages/business/tests/reply-limits.test.mjs"]), ("M2-incoming-human", [(WRITER, "actor === old.human", "actor === document.human")], ["packages/business/tests/reply-limits.test.mjs"]), ("M3-split-check-transaction", [(BROKER, ATOMIC, ""), (BROKER, REENTER, SPLIT)], ["packages/bus/tests/citation-guard.test.mjs"]), ("M5-hide-library-unavailable", [("packages/library/src/guard.mjs", "refuse('citation-unavailable', 'The library is busy or unavailable. Retry the answer when the library is available.');", "served = null;")], ["packages/bus/tests/citation-availability.test.mjs"]), ("M4-drop-client-reason", [(CLIENT, "new BusError(r.error, typeof r.reason === 'string' ? r.reason.slice(0, 300) : undefined)", "new BusError(r.error)")], ["packages/bus/tests/citation-guard.test.mjs"]), ] archive = subprocess.check_output(["git", "archive", "HEAD", "packages", "roles", "scripts", "tools", "docs", "package.json", "RELEASE"], cwd=ROOT) summary = [] selected = set(sys.argv[2:]) product_files = [line.split(' ', 1)[1] for line in (ROOT / 'agents/beakley/work/queue-217/manifest.r5.sha256').read_text().splitlines()] def hashes(base, files): return {file: hashlib.sha256((base / file).read_bytes()).hexdigest() for file in files} original_products = hashes(ROOT, product_files) for name, changes, tests in MUTANTS: if selected and name not in selected: continue with tempfile.TemporaryDirectory(prefix="row217-mutant-") as directory: work = Path(directory) with tarfile.open(fileobj=io.BytesIO(archive)) as source: source.extractall(work, filter="data") modules = ROOT / "node_modules" if modules.is_symlink(): raise SystemExit('root node_modules must be a local install, never a canonical link') if modules.exists(): os.symlink(modules.resolve(), work / "node_modules", target_is_directory=True) cmd = ["timeout", "-k", "10", "120", "node", "--test", *tests] with (OUT / f"{name}-control.log").open("w") as log: control = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=120) if control.returncode: raise SystemExit(f"{name}: control RED, no mutant result claimed") originals = {file: (work / file).read_bytes() for file, _, _ in changes} try: for file, before, after in changes: path = work / file text = path.read_text() if text.count(before) != 1: raise SystemExit(f"{name}: expected exactly one mutation anchor in {file}") path.write_text(text.replace(before, after)) with (OUT / f"{name}.log").open("w") as log: mutant = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=140) if mutant.returncode != 1: raise SystemExit(f"{name}: expected assertion failure exit 1, got {mutant.returncode}") finally: for file, content in originals.items(): (work / file).write_bytes(content) assert all((work / file).read_bytes() == content for file, content in originals.items()) assert hashes(ROOT, product_files) == original_products, 'checkout product hashes changed' (OUT / f"{name}-restored.sha256").write_text(''.join(f'{digest} {file}\n' for file, digest in sorted(original_products.items()))) with (OUT / f"{name}-restored-green.log").open("w") as log: restored = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=140) if restored.returncode: raise SystemExit(f"{name}: restored control RED") summary.append(f"{name}: killed (control green, mutant exit {mutant.returncode}, hashes restored, restored green)") (OUT / "summary.txt").write_text("\n".join(summary) + "\n") print(summary[-1], flush=True)