Files
stack/agents/researcher/work/queue-99/r1/mutants.py
T
jason.woltjeandClaude Opus 5.5 8ac8055c4d researcher: row 99 round 1 packet, text-mode inbox fallback, phase B green (#1593)
Built by a Haiku subagent (decision 129). cli 133/0, nine scripts green,
7 of 7 mutants killed. Manifest 5c979e49.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 09:02:24 -05:00

170 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""Row 99 mutants. PHASE B ONLY: not run in phase A.
Usage: python3 -I mutants.py <worktree-root> <out-dir>
Runs packages/cli/tests/cli.test.mjs on the unmutated source first (baseline),
then once per mutant of packages/cli/src/cli.mjs. Each mutant is one exact text
replacement that must match exactly once. A mutant counts as killed only when
its run exits nonzero, did not time out, and the named test reports "not ok".
A timeout, a missing match, or a run that passes counts as NOT killed.
Each run starts in its own process group. The group is killed after the run,
and nothing outside that group is signalled. The source is restored in a
finally block and checked against the candidate hash.
"""
import hashlib
import json
import os
import pathlib
import signal
import subprocess
import sys
ROOT = pathlib.Path(sys.argv[1]).resolve()
OUT = pathlib.Path(sys.argv[2]).resolve()
SRC = ROOT / "packages/cli/src/cli.mjs"
TEST = "packages/cli/tests/cli.test.mjs"
TIMEOUT_S = 120
CANDIDATE_CLI_SHA256 = "afcb9d9276c96e543cdc4dac1856b2dd6e973dfa2f1c68d179ddb5fc0e25b1ad"
ENV = dict(os.environ, DOCKER_HOST="unix:///nonexistent.sock")
T_A = "text-mode inbox on a bus without the messages verb prints the decisions and the unavailable line, exit 0"
T_B = "text-mode inbox still fails on any other messages error, and prints nothing"
T_C = "text-mode inbox prints the unread count and every unread message across pages"
T_D = "a later page answered unknown-verb drops the partial list and prints only the unavailable line"
FALLBACK = r'return io.stdout.write(`${formatInbox(decisions)}\nmessages: not available until the bus restarts\n`);'
# (name, old text, new text, the named test that must die)
MUTANTS = [
(
"M1-remove-fallback",
r'if (e.code !== "unknown-verb") throw e;',
"throw e;",
T_A,
),
(
"M2-widen-to-every-error",
r'if (e.code !== "unknown-verb") throw e;',
"if (false) throw e;",
T_B,
),
(
"M3-changed-line-text",
"messages: not available until the bus restarts",
"messages: unavailable",
T_A,
),
(
"M4-drop-decisions-in-fallback",
r"${formatInbox(decisions)}\nmessages: not available",
r"\nmessages: not available",
T_A,
),
(
"M5-fallback-exits-nonzero",
FALLBACK,
r'{ io.stdout.write(`${formatInbox(decisions)}\nmessages: not available until the bus restarts\n`); '
r'throw new CliError("messages not available", 1); }',
T_A,
),
(
"M7-read-messages-counted-as-unread",
"messages.push(...page.messages.filter(m => !m.read));",
"messages.push(...page.messages);",
T_C,
),
(
"M6-partial-list-printed-as-count",
FALLBACK,
r"return print({ decisions, messages }, value => formatInbox(value.decisions) + "
r"`\nmessages: ${value.messages.length} unread\n` + value.messages.map(formatMessage).join(''));",
T_D,
),
]
def group_alive(pgid):
try:
os.killpg(pgid, 0)
return True
except ProcessLookupError:
return False
def run_suite(label):
proc = subprocess.Popen(
["node", "--test", "--test-reporter=tap", TEST],
cwd=ROOT,
env=ENV,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
start_new_session=True,
)
timed_out = False
try:
out, _ = proc.communicate(timeout=TIMEOUT_S)
except subprocess.TimeoutExpired:
timed_out = True
os.killpg(proc.pid, signal.SIGKILL)
out, _ = proc.communicate()
finally:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
text = out.decode("utf8", "replace")
(OUT / f"{label}.log").write_text(text, encoding="utf8")
return {
"label": label,
"exit": proc.returncode,
"timed_out": timed_out,
"not_ok": [line.strip() for line in text.splitlines() if "not ok" in line],
"group_clear": not group_alive(proc.pid),
}
def killed(res, test_name):
return (not res["timed_out"]) and res["exit"] != 0 and any(test_name in line for line in res["not_ok"])
def main():
OUT.mkdir(parents=True, exist_ok=True)
original = SRC.read_bytes()
if hashlib.sha256(original).hexdigest() != CANDIDATE_CLI_SHA256:
print("refusing: cli.mjs is not the candidate hash", file=sys.stderr)
return 2
rows = []
try:
base = run_suite("baseline")
rows.append({**base, "status": "baseline"})
if base["exit"] != 0 or base["timed_out"] or base["not_ok"]:
print("baseline is not green; no mutants run", file=sys.stderr)
return 2
text = original.decode("utf8")
for name, old, new, test_name in MUTANTS:
if text.count(old) != 1:
rows.append({"label": name, "status": "no-match (counted as not killed)"})
continue
SRC.write_bytes(text.replace(old, new, 1).encode("utf8"))
res = run_suite(name)
if res["timed_out"]:
status = "TIMEOUT (counted as failure, not killed)"
else:
status = "killed" if killed(res, test_name) else "NOT killed"
rows.append({**res, "status": status, "named_test": test_name})
finally:
SRC.write_bytes(original)
restored = hashlib.sha256(SRC.read_bytes()).hexdigest() == CANDIDATE_CLI_SHA256
rows.append({"label": "restore", "status": "restored to candidate" if restored else "RESTORE FAILED"})
(OUT / "results.json").write_text(json.dumps(rows, indent=2) + "\n", encoding="utf8")
for row in rows:
print(f"{row['label']}: {row['status']}")
all_killed = all(r.get("status") == "killed" for r in rows if r["label"].startswith("M"))
return 0 if all_killed and rows[-1]["status"] == "restored to candidate" else 1
if __name__ == "__main__":
sys.exit(main())