Files
2026-10-23 23:57:31 -05:00

68 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Slot-only runner. Every mutation restores and verifies the complete broker bytes."""
from pathlib import Path
import hashlib, os, re, subprocess, sys
ROOT = Path(__file__).resolve().parents[4]
SOURCE = ROOT / 'packages/bus/src/broker.mjs'
OUT = Path(__file__).resolve().parent / 'logs'
MUTANTS = [
('M01-human', 'human-required', 'room verbs require the human', ' this.#human(s);\n', ''),
('M02-tester', 'tester-refused', 'room verbs require the human', None, None),
('M03-project', 'project-not-found', 'room verbs refuse missing projects', " if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found');\n", ''),
('M04-members', 'thread-instance-missing', 'room verbs refuse missing projects', " if (!members.length) fail('thread-instance-missing');\n", ''),
('M05-collision', 'room-thread-name-taken', 'room.open preflights instance collisions', " if (t && (t.instance !== instance || t.owner !== owner))\n fail(opening ? 'room-thread-name-taken' : 'room-not-open');\n", ''),
('M06-open', 'room-not-open', 'send/read refuse a missing canonical member', " if (!t && !opening) fail('room-not-open');\n", ''),
('M07-body', 'invalid-request', 'unknown or non-member addressees and tags-only bodies', " if (!addressed.body.trim()) fail('invalid-request');\n", ''),
('M08-unknown', 'unknown-addressee', 'unknown or non-member addressees and tags-only bodies', " if (!candidates.size) fail('unknown-addressee');\n", ''),
('M09-ambiguous', 'addressee-ambiguous', 'a legacy name colliding with another member key', " if (candidates.size > 1) fail('addressee-ambiguous');\n", ''),
('M10-messaging', 'message-sender-refused', 'messaging denial on the last target', " this.#checkMessaging(s.business, 'human', t.instance);\n", ''),
('M11-cursor', 'message-not-found', 'room.read groups one send, pages', " if (after === undefined) fail('message-not-found');\n", ' if (after === undefined) after = 0;\n'),
('M12-fence', 'session-clearing', 'a clearing last member fences the whole send', ' this.#sessionFence(session);\n', ''),
]
def sha(data): return hashlib.sha256(data).hexdigest()
def main():
OUT.mkdir(exist_ok=True)
original = SOURCE.read_bytes()
text = original.decode()
start = text.index(" case 'room.open':")
end = text.index(" // A thread belongs", start)
block = text[start:end]
for name, refusal, expected, old, new in MUTANTS:
if old is None:
needle = 'export const TESTER_VERBS = Object.freeze(['
assert text.count(needle) == 1
changed = text.replace(needle, needle + "\n 'room.open', 'room.send', 'room.read',", 1)
else:
assert block.count(old) == 1, (name, block.count(old))
changed = text[:start] + block.replace(old, new, 1) + text[end:]
# Save conventional original→mutant diff without relying on shell interpolation.
import difflib
(OUT / f'{name}.diff').write_text(''.join(difflib.unified_diff(text.splitlines(True), changed.splitlines(True), fromfile='broker.original', tofile=name)))
tmp = subprocess.check_output(['mktemp', '-d', '/mnt/storage/scratch/tmp/XXXXXX'], text=True).strip()
env = {**os.environ, 'TMPDIR': tmp}
log = OUT / f'{name}.log'
try:
SOURCE.write_text(changed)
with log.open('w') as stream:
for command in [['date', '-u'], ['uptime'], ['bash', '-c', 'bash /mnt/storage/scratch/sage-busy-now.sh 2>/dev/null | tail -1']]:
subprocess.run(command, cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, check=True)
stream.write(f'TMPDIR={tmp}\nmutant={name} refusal={refusal} original_sha256={sha(original)}\n')
stream.flush()
result = subprocess.run(['timeout', '-k', '10', '120', 'node', '--test', '--test-reporter=tap', 'packages/bus/tests/rooms.test.mjs'],
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT)
output = log.read_text()
killed = result.returncode == 1 and re.search(r'(?m)^not ok \d+ - ' + re.escape(expected), output)
print(f'{name}: exit={result.returncode} target_killed={bool(killed)} log={log}', flush=True)
if not killed: return 1
finally:
SOURCE.write_bytes(original)
assert SOURCE.read_bytes() == original
assert sha(SOURCE.read_bytes()) == sha(original)
with log.open('a') as stream: stream.write(f'RESTORED sha256={sha(original)}\n')
return 0
if __name__ == '__main__': sys.exit(main())