68 lines
4.7 KiB
Python
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())
|