Files

69 lines
6.1 KiB
Python
Executable File

#!/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')
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','/* missing validation */\n const old',B)
add('02-atomic-publication',p,'renameSync(candidate,file);','/* never publish candidate */',B)
add('03-previous-version',p,'durable(previous,oldBytes);',"durable(previous,'{}');",B)
add('04-authenticated-actor',p,'const event={transaction,actor,business:id',"const event={transaction,actor:'anonymous',business:id",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,'Object.hasOwn(limits.providers??{},provider)?limits.providers[provider]:undefined','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(v)||v<0','!Number.isSafeInteger(v)',B)
add('14-no-policy-ceiling',p,'!Number.isSafeInteger(v)||v<0','!Number.isSafeInteger(v)||v<0||v>4',B)
add('15-exact-provider-count',p,'s.provider!==' , 's.provider!==',B) # replaced below with an effective fault
cases[-1].update(old='pc.set(s.provider,(pc.get(s.provider)??0)+1)',new="pc.set('anthropic',(pc.get('anthropic')??0)+1)")
p='packages/harness/src/bundle.mjs'
add('16-provider-routing',p,'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}:{})','/* no hit details */',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)
p='packages/cli/src/limits.mjs'
add('21-print-only-migration',p,'io.stdout.write(JSON.stringify(migrateLaunch(raw,mapping,file,{rolesDir}),null,2)',"io.stdout.write(JSON.stringify((await import('node:fs')).writeFileSync(file,JSON.stringify(raw))??migrateLaunch(raw,mapping,file,{rolesDir}),null,2)",C)
p='packages/bus/src/broker.mjs'
add('22-human-only-settings',p,"if(!s.human)fail('human-required');if(!this.#organization)","if(false)fail('human-required');if(!this.#organization)",H)
p='packages/webui/src/serve.mjs'
add('23-settings-origin',p,"if(one(req,'origin')!==`https://${preview.host}`)return refuse(403,'cross-origin request refused','settings need the Console origin');","/* origin omitted */",W)
add('24-settings-fetch-metadata',p,"if(one(req,'sec-fetch-site')!=='same-origin')return refuse(403,'cross-site request refused','settings need same-origin Fetch Metadata');","/* fetch metadata omitted */",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(k=>!['revision','limits','models','instances'].includes(k))",'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;",'/* ignored model table update */',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)
# 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)