#!/usr/bin/env python3 import datetime,json,os,pathlib,socket,struct,subprocess,time from card_virtual_keyboard import Keyboard R=pathlib.Path('/run/k230-card-composition/session');O=pathlib.Path('/run/card-board-evidence');O.mkdir(exist_ok=True) os.environ.update(XDG_RUNTIME_DIR=str(R),SWAYSOCK=str(R/'sway-ipc.sock'),WAYLAND_DISPLAY='wayland-1') result={'evidence_class':'board-injected','source_revision':'310dbf131e81bcb5f455299a61baf4ad19a1ea53','harness_revision':'260197de09edfed54cc451a46ac815cad229b00c','probe':'/nix/store/wjzw7jw21p8qq4dsbc8b3fky9233pgqq-k230-card-composition-probe','at_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),'boot_id':pathlib.Path('/proc/sys/kernel/random/boot_id').read_text().strip(),'checks':{},'limits':['IPC-injected touch, no real-finger proof','Virtual keyboard, not physical OSK','Native captures do not prove optical cadence']} def record(k,v=True): result['checks'][k]=v;(O/'result.json').write_text(json.dumps(result,indent=2)+'\n');print('PASS',k,flush=True) def wait(f,seconds=20): end=time.monotonic()+seconds while time.monotonic()b[i] for a,b in zip(after,before) for i in (0,1)),(before,after) record('both_parent_and_subsurface_live',{'before':before,'after':after});shot('two-live-cards') cmd('down 10 284 200');cmd('down 11 284 700');cmd('up 10');cmd('up 11');wait(lambda:'reason=second-contact' in logs());record('second_contact_cancel') cmd('enter');cmd('cancel');wait(lambda:'reason=touch-cancel' in logs());record('touch_cancel') cmd('enter');cmd('down 1 284 700') for y in range(700,770,5):cmd('motion 1 284 '+str(y));time.sleep(.025) shot('card-during-drag');cmd('up 1');record('continuous_injected_motion') cmd('down 2 284 700');cmd('up 2');wait(lambda:'selected-expanded card=1' in logs());wait(lambda:focused()=='k230.card.two');kbd.press();wait(lambda:counters('two')[2]>=1);record('adjacent_expand_and_keyboard_focus');shot('expanded-second') cmd('enter');cmd('down 3 284 700');cmd('motion 3 284 500');cmd('up 3');wait(lambda:'close-refused card=1' in logs());assert focused()=='k230.card.two';kbd.press();wait(lambda:counters('two')[2]>=2);record('refused_close_recovers_focus') cmd('enter');cmd('down 4 284 200');cmd('motion 4 284 40');cmd('up 4');wait(lambda:'app-exit card=0' in logs());wait(lambda:focused()=='k230.card.two');kbd.press();wait(lambda:counters('two')[2]>=3);record('accepted_close_exit_focus_fallback');shot('after-close') record('presentation_signal_recorded','output-presented=' in logs());assert result['checks']['presentation_signal_recorded'] result['status']='PASS' except Exception as e: result['status']='FAIL';result['error']=type(e).__name__+': '+str(e);raise finally: if kbd:kbd.close() (O/'result.json').write_text(json.dumps(result,indent=2)+'\n')