Files

57 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Exact, reversible mutations of this owned worktree only. No product commit changes.
import argparse,datetime,hashlib,json,os,pathlib,re,subprocess,sys
p=argparse.ArgumentParser();p.add_argument('mutant',choices=['M1','M2','M3']);p.add_argument('--heavy-slot',action='store_true');args=p.parse_args()
if args.mutant=='M3' and not args.heavy_slot:p.error('M3 held until227 SLOTBACK; then require --heavy-slot')
folder=pathlib.Path(__file__).resolve().parent;profile=folder.parent/'run-targeted.py'
repo=pathlib.Path(subprocess.check_output(['git','rev-parse','--show-toplevel'],cwd=folder,text=True).strip())
anchors={
'M1':{'name':'proxy mode still passes CLAUDE_CODE_OAUTH_TOKEN','source':'packages/seat/src/session.mjs','test':'packages/seat/tests/proxy-session-env.test.mjs','old':'!(proxy && isProviderVariable(k))','new':'!(proxy && isProviderVariable(k) && k !== "CLAUDE_CODE_OAUTH_TOKEN")','bound':180},
'M2':{'name':'models.json gets literal key instead of environment reference','source':'packages/plugins/harness/pi/index.mjs','test':'packages/harness/tests/proxy-bundle.test.mjs','old':'writeFileSync(file, text, { mode: 0o600, flag: "wx" });','new':'writeFileSync(file, text.replaceAll("$ANTHROPIC_API_KEY", proxy.key), { mode: 0o600, flag: "wx" });','bound':180},
'M3':{'name':'proxy key serialized into session.json','source':'packages/cli/src/launcher.mjs','test':'packages/cli/tests/proxy-context-join.test.mjs','old':'bundle: files,\n harnessEnv,','new':'bundle: files,\n harnessEnv: bundle.env,','bound':300},
}
case=anchors[args.mutant];source=repo/case['source'];original=source.read_bytes();old=case['old'].encode();new=case['new'].encode()
pins=json.loads((folder.parent/'source-pins.json').read_text());pin={r['file']:r['sha256'] for r in pins}
def verify_all():
for file,want in pin.items():
if hashlib.sha256((repo/file).read_bytes()).hexdigest()!=want:raise RuntimeError('source identity mismatch:'+file)
verify_all()
if original.count(old)!=1:raise RuntimeError('exact anchor must match once')
if subprocess.run(['git','diff','--quiet','HEAD','--',case['source']],cwd=repo).returncode:raise RuntimeError('source dirty before mutation')
out=folder/args.mutant;out.mkdir(mode=0o700)
receipt={'mutant':args.mutant,**case,'product':'d6dc04e84fac839b7259488162b145b9683f943b','head':subprocess.check_output(['git','rev-parse','HEAD'],cwd=repo,text=True).strip(),'originalSha256':hashlib.sha256(original).hexdigest(),'anchorMatches':1,'started':datetime.datetime.now(datetime.timezone.utc).isoformat(),'phases':[]}
def run(phase):
tmp=subprocess.check_output(['mktemp','-d','/mnt/storage/scratch/tmp/gXXX'],text=True).strip()
result=subprocess.run([sys.executable,str(profile),case['test'],str(case['bound'])],cwd=repo,env={'PATH':'/usr/bin:/bin','TMPDIR':tmp,'LANG':'C.UTF-8'},stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=case['bound']+15)
text=result.stdout.decode(errors='replace')
# Only made-up unit-test values can occur, but never retain their values in receipts.
text=re.sub(r'fx332-[A-Za-z0-9_-]+','[fake-value-redacted]',text)
(out/(phase+'.txt')).write_text(text)
def count(label):
match=re.search(r'^ℹ '+label+r' (\d+)$',text,re.M);return int(match.group(1)) if match else None
info={'phase':phase,'exit':result.returncode,'pass':count('pass'),'fail':count('fail'),'skip':count('skipped'),'cancelled':count('cancelled'),'assertionFailure':'ERR_ASSERTION' in text}
receipt['phases'].append(info);return info
mutated=False
try:
baseline=run('baseline')
if baseline['exit']!=0 or baseline['fail']!=0 or baseline['skip']!=0:raise RuntimeError('baseline not green')
source.write_bytes(original.replace(old,new,1));mutated=True
receipt['mutantSha256']=hashlib.sha256(source.read_bytes()).hexdigest()
negative=run('mutant')
receipt['killed']=negative['exit']==1 and (negative['fail'] or 0)>0 and negative['assertionFailure'] and negative['cancelled']==0 and negative['skip']==0
if not receipt['killed']:raise RuntimeError('mutant did not produce the required assertion RED')
finally:
if mutated:source.write_bytes(original)
receipt['restoredSha256']=hashlib.sha256(source.read_bytes()).hexdigest()
receipt['byteIdentityRestored']=source.read_bytes()==original
verify_all()
receipt['finished']=datetime.datetime.now(datetime.timezone.utc).isoformat()
(out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
restored=run('restored')
receipt['restoredPositiveControl']=restored['exit']==0 and restored['fail']==0 and restored['skip']==0
verify_all()
receipt['allSourcePinsMatch']=True
receipt['finished']=datetime.datetime.now(datetime.timezone.utc).isoformat()
(out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
print(json.dumps(receipt,indent=2))
if not receipt['restoredPositiveControl']:raise SystemExit(1)