#!/usr/bin/env python3 """Isolated room mutants. Own worktree dependencies, assertion kills only, killpg after each run.""" import argparse, hashlib, json, os, signal, subprocess, tarfile, tempfile, time from pathlib import Path ROOT = Path(__file__).resolve().parents[4] UI = 'packages/webui/src/public/chat.js' HTTP = 'packages/webui/src/chat-http.mjs' BROKER = 'packages/bus/src/broker.mjs' PLUGIN = 'packages/plugins/channel/webui/index.mjs' SEAM = 'packages/webui/src/chat.mjs' BROWSER_TEST = 'packages/webui/tests/room-browser.test.mjs' HTTP_TEST = 'packages/webui/tests/room-http.test.mjs' BUS_TEST = 'packages/bus/tests/room-check.test.mjs' PLUGIN_TEST = 'packages/plugins/tests/room-channel.test.mjs' MUTANTS = [ ('M01-plugin', PLUGIN, ' "room.open",\n', '', PLUGIN_TEST), ('M02-seam-body', SEAM, "channel('room.send', { project, body })", "channel('room.send', { project, body: body.trim() })", HTTP_TEST), ('M03-page-cursor', HTTP, 'value.at(-1)?.ids.at(-1)', 'value.at(-1)?.id', HTTP_TEST), ('M04-room-status', HTTP, 'Object.keys(ROOM_LINES).map(code => [code, 409])', 'Object.keys(ROOM_LINES).map(code => [code, 400])', HTTP_TEST), ('M05-strict-body', HTTP, "!exact(v, ['body']) || ", '', HTTP_TEST), ('M06-receipt-type', HTTP, "typeof t.instance === 'string' && ", '', HTTP_TEST), ('M07-room-check', BROKER, 'return entries.map((m) => this.#withCheck(m));', 'return entries;', BUS_TEST), ('M08-room-last', BROKER, "['thread.messages', 'room.read'].includes(request.verb)", "['thread.messages'].includes(request.verb)", BUS_TEST), ('M09-browser-body', UI, 'request(`rooms/${c.id}/messages`, { body })', 'request(`rooms/${c.id}/messages`, { body: body.trim() })', BROWSER_TEST), ('M10-members', UI, 'catalog.rows.filter(m => threads.rows.some(t => t.project === id && t.instance === m.instance && t.name === m.instance))', 'catalog.rows', BROWSER_TEST), ('M11-checked-render', UI, 'messageBody({ ...m, from_role:', 'messageBody({ ...m, check: null, from_role:', BROWSER_TEST), ('M12-leading-tags', UI, 'let body = input.trimStart();', 'let body = input;', BROWSER_TEST), ('M13-readonly', UI, 'if (!roomOwner() || !c.canSend || !c.roomReady || out())', 'if (!roomOwner() || !c.roomReady || out())', BROWSER_TEST), ('M14-uncertainty', HTTP, 'write ? sendOutcome(err) :', "write ? { code: 'invalid-request', sent: false } :", HTTP_TEST), ('M15-pending', UI, ' if (s.pending) return;', '', BROWSER_TEST), ('M16-body-dedup', UI, 'new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.id, m]))', 'new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.body, m]))', BROWSER_TEST), ] def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest() def main(): p=argparse.ArgumentParser(); p.add_argument('--output',type=Path,required=True);p.add_argument('--only');a=p.parse_args() names=set(a.only.split(',')) if a.only else {m[0] for m in MUTANTS} assert names <= {m[0] for m in MUTANTS} assert not subprocess.check_output(['git','status','--porcelain','--','packages'],cwd=ROOT,text=True).strip(),'Commit tested packages first' assert (ROOT/'node_modules').is_dir() and not (ROOT/'node_modules').is_symlink(),'Local install required' before={f:sha(ROOT/f) for f in {m[1] for m in MUTANTS}} head=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip() a.output.mkdir(parents=True,exist_ok=True);results=[] with tempfile.TemporaryDirectory(prefix='fethry266-mutants-') as temp: temp=Path(temp);archive=temp/'source.tar' with archive.open('wb') as stream: subprocess.run(['git','archive','HEAD','packages','roles','contracts','tasks','missions','package.json','RELEASE'],cwd=ROOT,stdout=stream,check=True) for name,target,old,new,test in MUTANTS: if name not in names:continue tree=temp/name;tree.mkdir() with tarfile.open(archive) as bundle:bundle.extractall(tree,filter='data') (tree/'node_modules').symlink_to(ROOT/'node_modules',target_is_directory=True) path=tree/target;source=path.read_text();hits=source.count(old) assert hits==1,(name,'anchor hits',hits) path.write_text(source.replace(old,new,1));limit=240 if test==BROWSER_TEST else 120 cmd=['timeout','-k','10',str(limit),'node','--test','--test-reporter=tap','--test-concurrency=1','--test-timeout=30000',test] signals=[];started=time.monotonic() with (a.output/(name+'.tap')).open('w') as log: child=subprocess.Popen(cmd,cwd=tree,env={**os.environ,'MOSAIC_REQUIRE_EDITOR_BUILD':'1'},stdout=log,stderr=subprocess.STDOUT,start_new_session=True) try:code=child.wait(timeout=limit+15) except subprocess.TimeoutExpired:os.killpg(child.pid,signal.SIGKILL);signals.append('KILL');code=child.wait() finally: try: os.killpg(child.pid,signal.SIGTERM);signals.append('TERM');time.sleep(.2) try:os.killpg(child.pid,signal.SIGKILL);signals.append('KILL') except ProcessLookupError:pass except ProcessLookupError:pass path.write_text(source) restored=sha(path)==hashlib.sha256(source.encode()).hexdigest();unchanged=before=={f:sha(ROOT/f) for f in before} assert restored and unchanged,(name,'restore changed') output=(a.output/(name+'.tap')).read_text() killed=code==1 and 'ERR_ASSERTION' in output and 'not ok ' in output and "failureType: 'hookFailed'" not in output and '# cancelled 0\n' in output and '# skipped 0\n' in output result={'mutant':name,'test':test,'exit':code,'killed':killed,'hits':hits,'restored':restored,'candidateUnchanged':unchanged,'seconds':round(time.monotonic()-started,3),'cleanupSignals':signals} results.append(result);print(json.dumps(result),flush=True) if not killed:break report={'source':head,'selected':sorted(names),'candidateUnchanged':before=={f:sha(ROOT/f) for f in before},'results':results} (a.output/'summary.json').write_text(json.dumps(report,indent=2)+'\n') if len(results)!=len(names) or not all(r['killed'] for r in results):raise SystemExit(1) if __name__=='__main__':main()