#!/usr/bin/env python3
"""Capture a settled, matching pair from synthetic W1 app threads.
ACP stub harness; no live Antigravity backend.
Each invocation writes a timestamped run; it never removes previous evidence.
Usage: still-driver-v2.py before|after URL --root CAPTURE_DIR [--pair-log PRIVATE_LOG]
Volatile regions (vary between runs by nature, not by the PR): the sidebar age,
the failure row's wall-clock time, and the provider-update action.
"""
import argparse,datetime,pathlib,subprocess,json,time,re,os
from PIL import Image,ImageChops
p=argparse.ArgumentParser();p.add_argument('side');p.add_argument('url');p.add_argument('--pair-log');p.add_argument('--root',required=True);args=p.parse_args()
ROOT=pathlib.Path(args.root)
run=ROOT/(args.side+'-stills-'+datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ'));run.mkdir()
cli=str(ROOT/'browser-tools/node_modules/.bin/agent-browser');session='capture-33596-f6983c250bd9'
def command(*parts):
 r=subprocess.run([cli,'--session',session,*parts],capture_output=True,text=True)
 if r.returncode: raise RuntimeError(r.stdout+r.stderr)
 return r.stdout
if args.pair_log:
 text=pathlib.Path(args.pair_log).read_text();token=re.search(r'(?:#token=|[?&]token=)([A-Za-z0-9_-]+)',text)[1]
 command('open',args.url.split('/',3)[0]+'//'+args.url.split('/')[2]+'/pair')
 command('fill','input',token);command('find','role','button','click','--name','Continue')
command('set','viewport','1440','900','2');command('set','media','dark')
command('open',args.url)
command('eval',(ROOT/'frozen-clock.js').read_text())
command('wait','--text','Bad Gateway' if args.side=='after' else 'Antigravity sent nothing for 15 minutes')
command('wait','--load','networkidle')
command('eval','document.fonts.ready.then(()=>true)')
command('wait','button[aria-label="Dismiss error"]')
command('focus','button[aria-label="Dismiss error"]')
command('press','Enter')
command('wait','--fn',"!document.querySelector('button[aria-label=\"Dismiss error\"]')")
command('eval',"[...document.querySelectorAll('*')].filter(e=>e.scrollHeight>e.clientHeight&&getComputedStyle(e).overflowY.match(/auto|scroll/)).forEach(e=>e.scrollTop=0); true")
# Let the app's minute clock subscription observe the frozen Date; the age it shows
# depends on when the run happened, so wait for a tick instead of a fixed string.
time.sleep(61)
# Freeze only the browser's displayed relative times, after the real watchdog fires.
# The isolated server and its timeout always use the real wall clock.
command('eval',"window.__captureOriginalDateNow=Date.now; Date.now=()=>1794272400000; document.activeElement?.blur(); true")
command('wait','--fn',"!document.body.innerText.includes('froze for')")
# Invalidate compositor layers left by an expired notification, then return to the row viewport.
command('set','viewport','1441','900','2');command('set','viewport','1440','900','2')
command('wait','--load','networkidle');command('eval','document.fonts.ready.then(()=>true)')
first=run/'sample-1.png';second=run/'sample-2.png'
command('screenshot',str(first));time.sleep(.5);command('screenshot',str(second))
a,b=Image.open(first),Image.open(second)
if ImageChops.difference(a.convert('RGB'),b.convert('RGB')).getbbox():
 raise RuntimeError('Frames are not pixel-identical; retain both and retry after settling')
# Preserve native 2x rendering and a CSS-pixel sized still.
b.save(run/(args.side+'-native.png'));b.resize((1440,900),Image.Resampling.LANCZOS).save(run/(args.side+'.png'))
metrics=command('eval',"JSON.stringify({ua:navigator.userAgent,viewport:[innerWidth,innerHeight],dpr:devicePixelRatio,scale:visualViewport.scale,font:getComputedStyle(document.documentElement).fontSize,theme:document.documentElement.className,fonts:document.fonts.status})")
(run/'environment.txt').write_text(metrics)
(run/'verification.json').write_text(json.dumps({'pixelIdentical':True,'separationMs':500,'nativePixels':b.size,'viewport':[1440,900],'theme':'dark','clock':'browser Date frozen at 2026-11-10T01:00:00Z after genuine wall-clock retirement; server unchanged'},indent=2))
print(run)
