Files

72 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""One fault per acceptance check. Sequential process groups; whole-group timeout cleanup."""
import json,os,pathlib,signal,subprocess,sys,time
ROOT=pathlib.Path(sys.argv[1] if len(sys.argv)>1 else '/mnt/storage/src/mosaic-stack-rocko-122-r2')
OUT=pathlib.Path(__file__).resolve().parent/'mutants';OUT.mkdir(exist_ok=True)
B='packages/business/tests/launch-settings.test.mjs';C='packages/cli/tests/limits.test.mjs';L='packages/cli/tests/launch-limits.test.mjs';H='packages/bus/tests/limits.test.mjs';W='packages/webui/tests/settings-write.test.mjs';F='packages/webui/tests/settings-form.test.mjs'
cases=[]
def add(id,path,old,new,test):cases.append(dict(id=id,path=path,old=old,new=new,test=test))
p='packages/business/src/organization.mjs'
add('01-whole-document-validation',p,'validateBusinessDocument(document, file, { rolesDir });\n const old','/* validation omitted */\n const old',B)
add('02-atomic-publication',p,'renameSync(candidate, file);','/* publication omitted */',B)
add('03-previous-version',p,'durable(previous, oldBytes);',"durable(previous, '{}');",B)
add('04-authenticated-actor',p,'transaction, actor, business: id, before, after,','transaction, actor: "anonymous", business: id, before, after,',B)
add('05-old-new-receipt',p,'changes: changes(old, document)','changes: []',B)
add('06-stale-revision',p,'if (revision !== before)','if (false)',B)
add('07-uncertain-recovery',p,'if (hash !== e.before && hash !== e.after)','if (false)',B)
p='packages/business/src/launch.mjs'
add('08-total-limit',p,'["total", limits.total, usage.total]','["total", undefined, usage.total]',B)
add('09-provider-limit',p,'own(limits.providers, provider)','undefined',B)
add('10-model-limit',p,'own(own(limits.models, provider), model)','undefined',B)
add('11-at-limit',p,'running >= limit','running > limit',B)
add('12-unset-unlimited',p,'["total", limits.total, usage.total]','["total", limits.total ?? 0, usage.total]',B)
add('13-nonnegative-counts',p,'!Number.isSafeInteger(value) || value < 0','!Number.isSafeInteger(value)',B)
add('14-no-policy-ceiling',p,'!Number.isSafeInteger(value) || value < 0','!Number.isSafeInteger(value) || value < 0 || value > 4',B)
add('15-exact-provider-count',p,'providerCounts.set(provider, (providerCounts.get(provider) ?? 0) + 1);','providerCounts.set("anthropic", (providerCounts.get("anthropic") ?? 0) + 1);',B)
add('16-provider-routing','packages/harness/src/bundle.mjs','provider:entry.provider',"provider:'anthropic'",B)
p='packages/cli/src/launcher.mjs'
add('17-reread-every-launch',p,'b=loadBusiness(host.business,{dir:configDir(env),rolesDir:rolesDir(env)});','/* cached policy */',L)
add('18-refusal-hit-logged',p,'...(e.limit?{limit:e.limit}:{})','/* omitted hit */',L)
p='packages/business/src/migrate-launch.mjs'
add('19-migration-preserve-counts',p,'limits.models[entry.provider][entry.model] = count;','limits.models[entry.provider][entry.model] = 0;',C)
add('20-migration-ambiguity',p,'if (used.has(tuple))','if (false)',B)
add('21-print-only-migration','packages/cli/src/limits.mjs','const converted = migrateLaunch(raw, mapping, file, { rolesDir, system });','(await import("node:fs")).writeFileSync(file, JSON.stringify(raw));\n const converted = migrateLaunch(raw, mapping, file, { rolesDir, system });',C)
add('22-human-only-settings','packages/bus/src/broker.mjs','if (!s.human) fail("human-required");','/* human check omitted */',H)
p='packages/webui/src/serve.mjs'
add('23-settings-origin',p,"if (one(req, 'origin') !== `https://${preview.host}`)",'if (false)',W)
add('24-settings-fetch-metadata',p,"if (one(req, 'sec-fetch-site') !== 'same-origin')",'if (false)',W)
add('25-settings-send-mode',p,'if(!send||!settingsWrite||!named)','if(!settingsWrite||!named)',W)
add('26-settings-strict-keys',p,"Object.keys(value).some(key => !['revision', ...writable].includes(key))",'false',W)
p='packages/webui/src/public/bus.js'
add('27-form-instances',p,'Object.entries(v.instances||{})','Object.entries({})',F)
add('28-form-limit-payload',p,"if(kind==='total')limits.total=n;","if(kind==='total')limits.total=1;",F)
add('29-migration-idempotent','packages/business/src/migrate-launch.mjs','if (!out.launch?.max)','if (false)',C)
add('30-zero-stops-new','packages/business/src/launch.mjs','limit !== undefined && running >= limit','limit !== undefined && limit > 0 && running >= limit',B)
add('31-instance-model-edit','packages/business/src/launch-settings.mjs','if (Object.hasOwn(args, "instances"))','if (false)',B)
add('32-model-table-edit','packages/business/src/launch-settings.mjs','if (Object.hasOwn(args, "models")) next.launch.models = args.models;','/* model table ignored */',B)
add('33-runtime-writer-wiring','packages/bus/src/runtime.mjs',"organization:settings?(id,verb,args,actor)=>verb==='limits'?settings.read(id):settings.set(id,args,actor):null,",'organization:null,',H)
p='packages/business/src/migrate-launch.mjs'
add('34-inherited-exact-model',p,'old = system?.["execution.model"];','old = models[mapping.instances[instance]].model;',C)
add('35-inherited-provider',p,'provider = system?.["execution.provider"];','provider = models[mapping.instances[instance]].provider;',C)
add('36-config-required','packages/cli/src/limits.mjs','const system = inherits ? systemVars(loadSystem({ env: io.env })) : null;','const system = inherits ? {"execution.provider":"anthropic","execution.model":"claude-opus-5-5"} : null;',C)
p='packages/bus/src/runtime.mjs'
add('37-recovery-not-at-start',p,'const settings=organization?launchSettings({...organization,sessions:()=>readSessions(dataRoot)}):null;','const settings=organization?launchSettings({...organization,sessions:()=>readSessions(dataRoot)}):null;\n if(settings)for(const id of Object.keys(businesses))settings.writer.recover(id);',H)
add('38-uncertain-writes-refuse','packages/business/src/organization.mjs',' recover(id);',' /* reconciliation omitted */',H)
# Apply all matching copies for repeated logging sites. Every fault must change source.
results=[]
for c in cases:
file=ROOT/c['path'];before=file.read_text();matches=before.count(c['old'])
if not matches:raise RuntimeError('missing mutation site '+c['id'])
file.write_text(before.replace(c['old'],c['new']))
try:
env={**os.environ,'DOCKER_HOST':'unix:///nonexistent.sock'}
with (OUT/(c['id']+'.log')).open('w') as log:
p=subprocess.Popen(['node','--test',c['test']],cwd=ROOT,env=env,stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try:rc=p.wait(timeout=120)
except subprocess.TimeoutExpired:os.killpg(p.pid,signal.SIGKILL);p.wait();rc='timeout'
verdict='KILLED' if isinstance(rc,int) and rc!=0 else 'SURVIVED' if rc==0 else 'TIMEOUT'
row={k:c[k] for k in ['id','path','test']};row.update(result=verdict,exit=rc,sites=matches);results.append(row);print(c['id'],verdict,rc,flush=True)
finally:file.write_text(before)
(OUT/'results.json').write_text(json.dumps(results,indent=2)+'\n')
sys.exit(0 if all(r['result']=='KILLED' for r in results) else 1)