57 lines
4.7 KiB
Python
57 lines
4.7 KiB
Python
# 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)
|