Stranded delivery list on the bus agents view. Built by Sage's Haiku
subagent (decision 129), rebased onto 57a2ddc5. Phase B, three runs:
the first failed the new test (node:sqlite null-prototype rows, fixed
by mapping to plain objects), the second stopped at cli on a missing
node_modules link, the third passed: 14 node suites, nine scripts,
15/15 mutants killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
231 lines
8.2 KiB
Python
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())
|