Files
stack/agents/researcher/work/queue-80/r2/mutants.py
T
jason.woltjeandClaude Sonnet 5.5 199984f29c researcher: row 80 round 2 packet, mechanical rebase onto row 93 (#1574)
Base da6f1166, r1 patch applied clean with --index, +/- and context lines identical.
Bus suite 127/127, mutants 15 of 15 killed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 10:55:33 -05:00

231 lines
8.2 KiB
Python

#!/usr/bin/env python3
# Row 80 (#1574) mutation runner. Written in Phase A. NOT RUN.
#
# Phase B usage, from the canonical checkout, after "slot clear":
# DOCKER_HOST=unix:///nonexistent.sock python3 -I <packet>/mutants.py <worktree> <packet>
#
# Every mutant rewrites one exact-once anchor in packages/bus/src/broker.mjs of the
# worktree, runs tests/agents.test.mjs in its own process group under a timeout, and
# restores the original bytes in a finally block. The manifest is checked before the
# baseline and after the last restore. A red baseline aborts before any mutant.
# A timeout is a failure, not a kill.
#
# Exit codes: 0 every mutant killed; 1 a mutant survived or a run timed out;
# 2 baseline red, timed out, or manifest mismatch; 3 an anchor was not found exactly once.
import hashlib
import os
import signal
import subprocess
import sys
TIMEOUT_S = 240
BROKER = 'packages/bus/src/broker.mjs'
SUITE = ['node', '--test', 'tests/agents.test.mjs']
EXTRA_ENV = {'DOCKER_HOST': 'unix:///nonexistent.sock'}
ERROR_MARKERS = ('SqliteError', 'RangeError', 'TypeError', 'ReferenceError', 'SyntaxError')
# (id, file, line in candidate, exact old text, exact new text, expected killer)
MUTANTS = [
('M01', BROKER, 47,
'export const STRANDED = 50;',
'export const STRANDED = 51;',
'agents.test.mjs:203 length 50'),
('M02', BROKER, 1048,
'ORDER BY m.seq LIMIT ${STRANDED}',
'ORDER BY m.seq DESC LIMIT ${STRANDED}',
'agents.test.mjs:204 oldest-first ids'),
('M03', BROKER, 1048,
'ORDER BY m.seq LIMIT ${STRANDED}`',
'ORDER BY m.seq`',
'agents.test.mjs:203 length 50'),
('M04', BROKER, 1048,
'd.holder_run IS NOT ? AND NOT EXISTS',
'(d.holder_run IS NOT ? OR 1=1) AND NOT EXISTS',
'agents.test.mjs:189 current holder still holds the unread one'),
('M05', BROKER, 1048,
'd.holder_run IS NOT ? AND NOT EXISTS',
'd.holder_run = ? AND NOT EXISTS',
'agents.test.mjs:174 released-run entry'),
('M06', BROKER, 1048,
" AND NOT EXISTS(SELECT 1 FROM deliveries r WHERE r.message=m.id AND r.op='read') ORDER BY m.seq LIMIT ${STRANDED}",
" ORDER BY m.seq LIMIT ${STRANDED}",
'agents.test.mjs:192 read message must not be listed'),
('M07', BROKER, 1048,
'WHERE m.business=? AND m.to_role=? AND d.holder_run IS NOT ?',
'WHERE (? IS NULL OR 1=1) AND m.to_role=? AND d.holder_run IS NOT ?',
'agents.test.mjs:230 per business'),
('M08', BROKER, 1048,
'WHERE m.business=? AND m.to_role=? AND d.holder_run IS NOT ?',
'WHERE m.business=? AND (? IS NULL OR 1=1) AND d.holder_run IS NOT ?',
'agents.test.mjs:221 per role'),
('M09', BROKER, 1048,
'd.holder_run AS run, d.at FROM',
'd.holder_run AS holder, d.at FROM',
'agents.test.mjs:174 entry shape'),
('M10', BROKER, 1048,
'm.from_role AS "from"',
'm.from_role AS "sender"',
'agents.test.mjs:174 entry shape'),
('M11', BROKER, 1048,
'd.holder_run AS run, d.at FROM',
'd.holder_run AS run FROM',
'agents.test.mjs:174 entry shape'),
('M12', BROKER, 1053,
'return { ...c, state, working, inputNeeded, stranded };',
'return { ...c, state, working, inputNeeded };',
'agents.test.mjs:58 key list'),
('M13', BROKER, 932,
'return s.human || s.reader ? claims.map((c) => this.#agentState(c)) : claims;',
'return claims.map((c) => this.#agentState(c));',
'agents.test.mjs:62 role session keys'),
('M14', BROKER, 1048,
"JOIN deliveries d ON d.message=m.id AND d.op='delivered' WHERE m.business=? AND m.to_role=? AND d.holder_run IS NOT ?",
"LEFT JOIN deliveries d ON d.message=m.id AND d.op='delivered' WHERE m.business=? AND m.to_role=? AND d.holder_run IS NOT ?",
'agents.test.mjs:239 routed, not yet received, must not be listed'),
('M15', BROKER, 1052,
").map((m) => ({ id: m.id, from: m.from, run: m.run, at: m.at }));",
");",
'agents.test.mjs:174 prototype check'),
]
def sha256_of(path):
digest = hashlib.sha256()
with open(path, 'rb') as handle:
for block in iter(lambda: handle.read(65536), b''):
digest.update(block)
return digest.hexdigest()
def read_manifest(packet):
entries = []
with open(os.path.join(packet, 'candidate-manifest.sha256'), encoding='utf-8') as handle:
for line in handle:
line = line.rstrip('\n')
if line:
digest, rel = line.split(' ', 1)
entries.append((digest, rel))
return entries
def manifest_mismatches(wt, entries):
return [rel for digest, rel in entries if sha256_of(os.path.join(wt, rel)) != digest]
def kill_group(pgid):
try:
os.killpg(pgid, signal.SIGKILL)
except ProcessLookupError:
pass
def run_suite(wt, log_path):
env = dict(os.environ, **EXTRA_ENV)
with open(log_path, 'wb') as log:
proc = subprocess.Popen(
SUITE,
cwd=os.path.join(wt, 'packages', 'bus'),
env=env,
stdout=log,
stderr=subprocess.STDOUT,
start_new_session=True,
)
try:
return proc.wait(timeout=TIMEOUT_S), 'exit'
except subprocess.TimeoutExpired:
kill_group(proc.pid)
proc.wait()
return None, 'timeout'
finally:
kill_group(proc.pid)
def worktree_leftovers(wt):
# Reads /proc cwd links only. Reports; kills nothing outside the run's own group.
found = []
for pid in filter(str.isdigit, os.listdir('/proc')):
try:
cwd = os.readlink(os.path.join('/proc', pid, 'cwd'))
except OSError:
continue
if cwd == wt or cwd.startswith(wt + os.sep):
found.append((pid, cwd))
return found
def main():
if len(sys.argv) != 3:
sys.stderr.write('usage: mutants.py <worktree> <packet_dir>\n')
return 2
wt = os.path.abspath(sys.argv[1])
packet = os.path.abspath(sys.argv[2])
out = os.path.join(packet, 'out')
os.makedirs(out, exist_ok=True)
entries = read_manifest(packet)
broken = manifest_mismatches(wt, entries)
if broken:
print('manifest mismatch before baseline:', broken)
return 2
rc, status = run_suite(wt, os.path.join(out, 'baseline.log'))
if status != 'exit' or rc != 0:
print('baseline red or timed out:', status, rc)
return 2
summary = ['baseline: green']
failures = []
anchor_error = False
for mid, rel, line, old, new, killer in MUTANTS:
path = os.path.join(wt, rel)
with open(path, 'rb') as handle:
original = handle.read()
text = original.decode('utf-8')
count = text.count(old)
if count != 1:
summary.append(f'{mid} {rel}:{line} ANCHOR count={count}; aborting run')
anchor_error = True
break
log_path = os.path.join(out, f'{mid}.log')
try:
with open(path, 'wb') as handle:
handle.write(text.replace(old, new, 1).encode('utf-8'))
rc, status = run_suite(wt, log_path)
finally:
with open(path, 'wb') as handle:
handle.write(original)
with open(log_path, 'r', encoding='utf-8', errors='replace') as handle:
log_text = handle.read()
if status == 'timeout':
verdict = 'TIMEOUT (counted as failure)'
failures.append(mid)
elif rc == 0:
verdict = 'SURVIVED'
failures.append(mid)
elif any(marker in log_text for marker in ERROR_MARKERS):
verdict = 'KILLED (error marker in log; check the reason)'
else:
verdict = 'KILLED'
summary.append(f'{mid} {rel}:{line} {verdict}; expected killer: {killer}')
restored = manifest_mismatches(wt, entries)
leftovers = worktree_leftovers(wt)
summary.append(f'manifest after restore: {"clean" if not restored else restored}')
summary.append(f'processes with cwd in worktree: {leftovers if leftovers else "none"}')
with open(os.path.join(out, 'summary.txt'), 'w', encoding='utf-8') as handle:
handle.write('\n'.join(summary) + '\n')
print('\n'.join(summary))
if anchor_error:
return 3
if restored:
return 2
if failures:
return 1
return 0
if __name__ == '__main__':
sys.exit(main())