#!/usr/bin/env python3 """STEP15A: synthetic production components on an owned headless Sway. CONTRACT: no inherited compositor, bus, clipboard, notifications or HOME. Copies add measurement hooks only; the production geometry remains unchanged. Artifacts contain only synthetic pixels and aggregate performance measurements. """ import json import os from pathlib import Path import signal import subprocess import sys import time REPO = Path(__file__).resolve().parents[2] sys.path.insert(0, str(REPO / 'tests/session_v2/fixtures')) from headless import Lab SOURCE = REPO / 'quickshell/.config/quickshell/labfy-sway' QS = str(Path.home() / '.local/opt/quickshell-qt612-popup/bin/quickshell') ART = Path(os.environ['STEP15A_ARTIFACTS']) ART.mkdir(parents=True, exist_ok=True, mode=0o700) def call(lab, path, name, *args): p = subprocess.run([QS, 'ipc', '-p', str(path), 'call', 'fixture', name, *map(str,args)], env=lab.env, capture_output=True, text=True, timeout=5) if p.returncode: raise RuntimeError('Synthetic IPC failed: ' + p.stderr[-1000:]) return json.loads(p.stdout) if p.stdout.strip() else None def wait(lab, path): end = time.monotonic()+12 while time.monotonic()r.internalId!==id)} function clearAll() {records=[]} } DateCenter {id:popup;barWindow:bar;clockItem:clockItem;notificationService:service} IpcHandler {target:"fixture" function state():string {return JSON.stringify(popup.metrics())} function setup(count:int,player:bool):void { popup.setPlayer(player?fakePlayer:null); service.records=Array.from({length:count},(_,i)=>({internalId:i+1,appName:"TEST_ONLY", summary:"TEST_ONLY notification "+i,body:"synthetic long body ".repeat(40),image:""})); popup.requestOpen(); } function remove():void {service.dismiss(1)} function clear():void {service.clearAll()} function incoming():void {service.records=[{internalId:999,appName:"TEST_ONLY",summary:"Incoming",body:""}].concat(service.records)} function hide():void {popup.requestClose()} function scroll():void {popup.scrollList()} } } '''.replace('ART',art.as_uri())) proc=lab.start([QS,'-p',str(fixture),'--no-color']) wait(lab,fixture) results=[] for scale in [1,1.4]: lab.command(f'output HEADLESS-1 scale {scale}') for player in [False,True]: for n in [0,1,5,200]: call(lab,fixture,'setup',n,str(player).lower());time.sleep(.25) m=call(lab,fixture,'state');m.update(scale=scale,notifications=n) results.append(m) if player: assert m['playerHeight']==116,m else: assert m['playerHeight']==0,m assert m['height']>=m['contentHeight']+32-1,('clipping',m) assert m['height']<=m['availableHeight']+1,('screen overflow',m) if n==200: call(lab,fixture,'scroll');time.sleep(.15) scrolled=call(lab,fixture,'state') assert scrolled['playerY']==m['playerY'] and scrolled['playerHeight']==m['playerHeight'] assert scrolled['list']['y']>0,scrolled assert scrolled['list']['y']+scrolled['list']['viewport']>=scrolled['list']['content']-2,scrolled if player: assert scrolled['art']['width']<=144 and scrolled['art']['height']<=144,scrolled if n in [0,200]: subprocess.run(['grim','-o','HEADLESS-1',str(ART/f'date-{scale}-{player}-{n}.png')],env=lab.env,check=True) call(lab,fixture,'setup',5,'true');call(lab,fixture,'remove');time.sleep(.15) results.append(dict(action='remove',**call(lab,fixture,'state'))) call(lab,fixture,'clear');time.sleep(.15) results.append(dict(action='clear',**call(lab,fixture,'state'))) call(lab,fixture,'incoming');time.sleep(.15) results.append(dict(action='incoming',**call(lab,fixture,'state'))) call(lab,fixture,'setup',0,'false');time.sleep(.15) results.append(dict(action='player-disappeared',**call(lab,fixture,'state'))) (ART/'date-metrics.json').write_text(json.dumps(results,indent=2)) call(lab,fixture,'hide') print(json.dumps({'dateCases':len(results),'lab':str(lab.directory)}))