Files

55 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""One deliberate fault per check. Restore bytes, isolate process groups, never count timeouts as kills."""
import hashlib,json,os,pathlib,signal,subprocess,sys
root=pathlib.Path('/mnt/storage/src/mosaic-stack-rocko-49')
out=pathlib.Path(__file__).resolve().parent/'mutants';out.mkdir(exist_ok=True)
restart='packages/tasks/tests/comment-restart.test.mjs'
cursors='packages/bus/tests/comment-cursors.test.mjs'
migration='packages/bus/tests/comment-migration.test.mjs'
cases=[]
def add(id,path,old,new,test): cases.append(dict(id=id,path=path,old=old,new=new,test=test))
adapter='packages/tasks/src/adapter.mjs';sync='packages/tasks/src/sync.mjs';broker='packages/bus/src/broker.mjs';store='packages/bus/src/store.mjs'
add('01-restore-durable-ids',adapter,'new Map(comments.cursors.map((c) => [c.task_id, c.last_comment]))','new Map()',restart)
add('02-persist-observed-ids',broker,'for (const x of commentCursors)', 'for (const x of [])',restart)
add('03-startup-comment-read',sync,'if (before === undefined || count !== before)','if (before !== undefined && count !== before)',restart)
add('04-exclude-bots',sync,'fresh && !ctx.bots.has(c.author.id)','fresh',restart)
add('05-strict-new-id',sync,'c.id > last','c.id >= last',restart)
add('06-cache-after-commit',sync,' if (out.snapshots.length || out.events.length || out.commentCursors.length)',
' for (const c of out.commentCursors) ctx.lastComment.set(c.task_id, c.last_comment);\n if (out.snapshots.length || out.events.length || out.commentCursors.length)',restart)
add('07-event-cursor-atomicity',sync,'ctx.broker.recordTask({ business: ctx.business, ...out });',
'ctx.broker.recordTask({ business: ctx.business, ...out, commentCursors: [] });\n if (out.commentCursors.length) ctx.broker.recordTask({ business: ctx.business, commentCursors: out.commentCursors });',restart)
add('08-no-empty-poll-row',sync,'if (top > (last ?? 0))','if (true)',restart)
add('09-initial-clock-margin',sync,'ctx.commentEpoch - WINDOW_MS','ctx.commentEpoch',restart)
add('10-durable-initial-epoch',adapter,'commentEpoch: Date.parse(comments.epoch)','commentEpoch: clock()',restart)
add('11-validate-cursor-input',broker,"if (![x.task_id, x.last_comment].every((n) => Number.isSafeInteger(n) && n > 0))",'if (false)',cursors)
add('12-sync-only-cursor-write',broker,"if (cap !== null && commentCursors.length) fail('agent-required');",'/* sync-only check omitted */',cursors)
add('13-business-isolation',broker,"ORDER BY task_id`, business,","ORDER BY task_id`, 'demo',",cursors)
add('14-explicit-offline-only',store,'if (!fresh && migrateSchema) this.#migrateSchema();','if (!fresh) this.#migrateSchema();',migration)
add('15-pinned-source-metadata',store,'this.get("SELECT value FROM meta WHERE key=\'schema_digest\'")?.value !== step.fromDigest ||','false ||',migration)
add('16-chain-all-steps',store,'while (version !== SCHEMA_VERSION)','if (version !== SCHEMA_VERSION)',migration)
add('17-preserve-source-copy',store,'this.#db.exec(step.sql);',"this.#db.exec(step.sql);\n this.#db.exec(\"DROP TRIGGER events_no_delete; DELETE FROM events; CREATE TRIGGER events_no_delete BEFORE DELETE ON events BEGIN SELECT RAISE(ABORT, 'events is append-only'); END\");",migration)
add('18-land-work-version','scripts/land-work/read.mjs',"meta('schema_version') !== SCHEMA_VERSION","meta('schema_version') !== '3b'",'scripts/test-land-work.sh')
add('21-exclusive-writer-lock',store,"fd = openSync(this.#lock, 'wx', 0o600);","fd = openSync(this.#lock, 'w', 0o600);",migration)
add('22-migration-command','packages/bus/src/migrate-schema.mjs','migrateSchema: true','migrateSchema: false',migration)
results=[]
for c in cases:
file=root/c['path'];before=file.read_bytes();source=before.decode();n=source.count(c['old'])
if n!=1: raise RuntimeError(f"{c['id']}: expected one mutation site, found {n}")
file.write_text(source.replace(c['old'],c['new']))
try:
command=['bash',c['test']] if c['test'].endswith('.sh') else ['node','--test',c['test']]
with (out/(c['id']+'.log')).open('w') as log:
p=subprocess.Popen(command,cwd=root,env={**os.environ,'DOCKER_HOST':'unix:///nonexistent.sock'},
stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try: rc=p.wait(timeout=180)
except subprocess.TimeoutExpired: os.killpg(p.pid,signal.SIGKILL);p.wait();rc='timeout'
content=(out/(c['id']+'.log')).read_text()
invalid=any(x in content for x in ['SyntaxError:', 'ERR_MODULE_NOT_FOUND', 'schema-source-mismatch', 'migration-target-mismatch'])
verdict='INVALID' if invalid else 'TIMEOUT' if rc=='timeout' else 'SURVIVED' if rc==0 else 'KILLED'
result={k:c[k] for k in ['id','path','test']};result.update(result=verdict,exit=rc)
results.append(result);(out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
print(c['id'],verdict,rc,flush=True)
finally:file.write_bytes(before)
if verdict!='KILLED':sys.exit(1)
sys.exit(0)