Files
stack/agents/darkwing/work/queue-106/r1/mutants.py
T
jason.woltjeandClaude Opus 5.5 7109e39e00 work: Darkwing row 106 round 1 packet (#1600)
Bus host lock follow-ups: release on a fork or send throw, retry a lock
that vanishes before the read, the have order test fixed to directory
order, the third-racer README sentence and the reviewer wrap. Gate passed
on 0069949ff; 9 mutant runs as expected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 17:50:20 -05:00

127 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Row 106 (#1600) mutants. Each takes out or bends one new check; every named
# (test file, name pattern) run must fail on it. A mutant may edit several
# files; each is restored from its .orig after the run, and the candidate's
# diff must hash the same at the end as at the start. The x mutants are
# controls: they run the base's `have` order test against n06 and n07, which
# it passes, since it takes the clone order from the request it checks.
import hashlib, os, signal, subprocess, sys, time
WT = os.path.expanduser("~/darkwing-scratch/r106/wt")
OUT = os.path.expanduser("~/darkwing-scratch/r106/mutants")
ENV = dict(os.environ, TMPDIR=os.path.expanduser("~/darkwing-scratch/r106/tmp"))
os.makedirs(OUT, exist_ok=True)
os.makedirs(ENV["TMPDIR"], exist_ok=True)
HO, GT = "packages/cli/src/host.mjs", "packages/harness/src/git-tools.mjs"
T_HO, T_GT = "packages/cli/tests/host.test.mjs", "packages/harness/tests/git-tools.test.mjs"
P_FORK, P_SEND = "^a broker fork that throws", "^a broker send that throws"
P_GONE, P_HAVE = "^a host.lock that goes between", "^fetch_review's have orders"
TRY = ''' try {
broker = fork(BROKER, [], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
broker.send({ op: "boot", config: boot });
ready = await firstReply(broker, bootTimeoutMs, "broker");'''
LOOP = ''' for (const remote of ["refs/remotes/working", "refs/remotes/origin"])
for (const project of projects) {
if (have.size >= HAVE_MAX) break;
const r = await git(["-C", join(workspace, project), "for-each-ref", "--sort=-committerdate", "--format=%(objectname)", remote], env);
if (r.ok) for (const oid of r.out.split("\\n")) if (OID.test(oid) && have.size < HAVE_MAX) have.add(oid);
}'''
DATE_SORT = (GT, LOOP, ''' for (const remote of ["refs/remotes/working", "refs/remotes/origin"]) {
const all = [];
for (const project of projects) {
const r = await git(["-C", join(workspace, project), "for-each-ref", "--format=%(committerdate:unix) %(objectname)", remote], env);
if (r.ok) for (const l of r.out.split("\\n")) { const [d, oid] = l.split(" "); if (OID.test(oid ?? "")) all.push([Number(d), oid]); }
}
all.sort((x, y) => y[0] - x[0]);
for (const [, oid] of all) if (have.size < HAVE_MAX) have.add(oid);
}''')
REVERSE = (GT, "const projects = projectsIn(workspace);", "const projects = projectsIn(workspace).reverse();")
BASE_TEST = (T_GT, None, subprocess.run(["git", "-C", WT, "show", "HEAD:" + T_GT], capture_output=True, text=True, check=True).stdout)
# (name, [(path, old, new)], [(test, pattern)], expect) with old None for a whole-file replacement.
M = [
("n01 the fork runs outside the try", [(HO, TRY, ''' broker = fork(BROKER, [], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
try {
broker.send({ op: "boot", config: boot });
ready = await firstReply(broker, bootTimeoutMs, "broker");''')], [(T_HO, P_FORK)], "killed"),
("n02 the send runs outside the try", [(HO, TRY, ''' try {
broker = fork(BROKER, [], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
} catch (e) {
release();
throw e;
}
broker.send({ op: "boot", config: boot });
try {
ready = await firstReply(broker, bootTimeoutMs, "broker");''')], [(T_HO, P_SEND)], "killed"),
("n03 the fork and send run outside the try, as at the base", [(HO, TRY, ''' broker = fork(BROKER, [], { stdio: ["ignore", "inherit", "inherit", "ipc"] });
broker.send({ op: "boot", config: boot });
try {
ready = await firstReply(broker, bootTimeoutMs, "broker");''')], [(T_HO, P_FORK), (T_HO, P_SEND)], "killed"),
("n04 a lock gone before the read refuses as unreadable", [(HO, ' if (e.code === "ENOENT") return null;\n', "")], [(T_HO, P_GONE)], "killed"),
("n05 any read failure counts as no lock", [(HO, 'if (e.code === "ENOENT") return null;', "return null;")], [(T_HO, P_GONE)], "killed"),
("n06 have takes the clones in reverse directory order", [REVERSE], [(T_GT, P_HAVE)], "killed"),
("n07 have sorts by date across clones", [DATE_SORT], [(T_GT, P_HAVE)], "killed"),
("x01 control: the base test against n06", [REVERSE, BASE_TEST], [(T_GT, P_HAVE)], "survived"),
("x02 control: the base test against n07", [DATE_SORT, BASE_TEST], [(T_GT, P_HAVE)], "survived"),
]
def diffhash():
d = subprocess.run(["git", "-C", WT, "diff", "--", ".", ":!node_modules"], capture_output=True).stdout
return hashlib.sha256(d).hexdigest()
def run(test, pattern, log):
pkg = os.path.join(WT, *test.split("/")[:2])
with open(log, "ab") as f:
f.write(f"== {test} /{pattern}/\n".encode()); f.flush()
p = subprocess.Popen(["node", "--test", f"--test-name-pattern={pattern}", os.path.relpath(os.path.join(WT, test), pkg)], cwd=pkg, env=ENV,
stdout=f, stderr=subprocess.STDOUT, start_new_session=True)
try:
rc = p.wait(timeout=600)
except subprocess.TimeoutExpired:
os.killpg(p.pid, signal.SIGKILL); p.wait(); return "timeout"
# A pattern that matched nothing passes with no tests; count that as a fault.
tail = open(log, encoding="utf8", errors="replace").read().split(f"== {test} /{pattern}/\n")[-1]
ran = sum(int(l.split()[2]) for l in tail.splitlines() if l.startswith(("ℹ pass ", "ℹ fail ")))
return "no-tests" if ran == 0 else rc
for _, edits, *_ in M:
for path, *_ in edits:
if os.path.exists(os.path.join(WT, path + ".orig")):
sys.exit(f"STOP {path}.orig exists")
start = diffhash()
print(f"start diff {start[:16]} {time.strftime('%H:%M:%S', time.gmtime())}", flush=True)
bad = []
only = set(sys.argv[1:])
for name, edits, tests, expect in M:
if only and name.split()[0] not in only: continue
srcs = {}
ok = True
for path, old, new in edits:
full = os.path.join(WT, path)
src = srcs.get(path) or open(full).read()
if old is not None and src.count(old) != 1:
print(f"{name}: ANCHOR {path} {src.count(old)}", flush=True); ok = False; break
srcs[path] = new if old is None else src.replace(old, new)
if not ok:
bad.append(name); continue
for path in srcs: os.rename(os.path.join(WT, path), os.path.join(WT, path + ".orig"))
try:
for path, text in srcs.items(): open(os.path.join(WT, path), "w").write(text)
log = os.path.join(OUT, name.split()[0] + ".log")
open(log, "w").close()
rcs = [run(t, p, log) for t, p in tests]
finally:
for path in srcs: os.replace(os.path.join(WT, path + ".orig"), os.path.join(WT, path))
if any(rc in ("timeout", "no-tests") for rc in rcs): got = "FAULT"
elif all(rc != 0 for rc in rcs): got = "killed"
elif all(rc == 0 for rc in rcs): got = "survived"
else: got = "MIXED"
print(f"{name}: {got} (expected {expect}) {list(zip([t.split('/')[-1] for t, _ in tests], rcs))}", flush=True)
if got != expect: bad.append(name)
end = diffhash()
print(f"end diff {end[:16]} {'same' if end == start else 'CHANGED'} {time.strftime('%H:%M:%S', time.gmtime())}", flush=True)
print("MUTANTS_OK" if not bad and end == start else f"MUTANTS_BAD {bad}")