From 34839df2225b6f842a4915ca1dd9bc1643a73e2f Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 17 Sep 2026 18:04:26 -0700 Subject: [PATCH] Retry transient empty native modal inspection reads --- slab/captutor/lib/modal-police.mjs | 18 ++++++++++++++++-- slab/captutor/test/modal-police.test.mjs | 13 ++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/slab/captutor/lib/modal-police.mjs b/slab/captutor/lib/modal-police.mjs index b88b177fc0..b68fa58de7 100644 --- a/slab/captutor/lib/modal-police.mjs +++ b/slab/captutor/lib/modal-police.mjs @@ -43,12 +43,26 @@ for(const h of hits){let position=null;try{position=h.element.position();}catch{ seen.add(key);physical.add(physicalKey); } `; +export async function readNativeModalScan(execute){ + for(let attempt=0;attempt<3;attempt++){ + const {stdout}=await execute(); + if(!stdout.trim()){ + if(attempt<2){await new Promise(resolve=>setTimeout(resolve,150));continue;} + throw Error('Native modal inspection returned no result after 3 scans'); + } + const hits=JSON.parse(stdout); + if(!Array.isArray(hits)||hits.some(h=>!h||typeof h.kind!=='string'||typeof h.title!=='string'||!Array.isArray(h.buttons)||h.buttons.some(b=>typeof b!=='string')))throw Error('Invalid native modal inspection result'); + return hits; + } +} async function native(action,expected) { const tail=action ? `const h=uniqueHits.filter(h=>h.kind===${JSON.stringify(action)});if(h.length!==1||!h[0].element)throw Error('Modal changed or action ambiguous');if(JSON.stringify([h[0].kind,h[0].title,[...h[0].buttons].sort()])!==${JSON.stringify(JSON.stringify(expected?[expected.kind,expected.title,[...expected.buttons].sort()]:null))})throw Error('Modal fingerprint changed');h[0].element.click();JSON.stringify(true);` : `JSON.stringify(uniqueHits.map(({kind,title,buttons})=>({kind,title,buttons})));`; - const {stdout}=await run('/usr/bin/osascript',['-l','JavaScript','-e',CHROME_MODAL_SCRIPT+tail],{timeout:10000,maxBuffer:128*1024}); - return JSON.parse(stdout); + const execute=()=>run('/usr/bin/osascript',['-l','JavaScript','-e',CHROME_MODAL_SCRIPT+tail],{timeout:10000,maxBuffer:128*1024}); + // Never replay a click. Only retry a read-only scan with an empty response. + if(!action)return readNativeModalScan(execute); + return JSON.parse((await execute()).stdout); } export function fingerprintModal(hit) { return createHash('sha256').update(JSON.stringify([hit.kind,hit.title,[...hit.buttons].sort()])).digest('hex').slice(0,20); diff --git a/slab/captutor/test/modal-police.test.mjs b/slab/captutor/test/modal-police.test.mjs index 0b51418ab9..d6a92f5dea 100644 --- a/slab/captutor/test/modal-police.test.mjs +++ b/slab/captutor/test/modal-police.test.mjs @@ -4,7 +4,7 @@ import {mkdtempSync,rmSync,readFileSync} from 'node:fs'; import {tmpdir} from 'node:os'; import {join} from 'node:path'; import {runInNewContext} from 'node:vm'; -import {CHROME_MODAL_SCRIPT,createModalPolice,connectWithModalPolice} from '../lib/modal-police.mjs'; +import {CHROME_MODAL_SCRIPT,createModalPolice,connectWithModalPolice,readNativeModalScan} from '../lib/modal-police.mjs'; const hit=kind=>({kind,title:kind,buttons:kind==='remote-debugging'?['Cancel','Allow']:['Close']}); function setup(t,kind,options={}){ const directory=mkdtempSync(join(tmpdir(),'modal-police-'));t.after(()=>rmSync(directory,{recursive:true,force:true})); @@ -61,3 +61,14 @@ test('connection watcher rescans a moved consent dialog instead of abandoning it }); test('a connection settled during native scan does not fail or click',async t=>{let pending=true;const f=setup(t,'automation-banner',{scan:async()=>{pending=false;return [hit('automation-banner')];}});assert.equal((await f.police.check('connecting',{mayHandle:()=>pending})).expired,true);assert.deepEqual(f.actions,[]);}); + +test('empty native scan retries without treating missing output as clean',async()=>{ + let calls=0;const hits=await readNativeModalScan(async()=>({stdout:++calls<2?'':'[{"kind":"unknown","title":"Blocking sheet","buttons":[]}]'}));assert.equal(calls,2);assert.equal(hits[0].kind,'unknown'); +}); +test('persistent empty native scan still blocks recording',async()=>{ + let calls=0;await assert.rejects(readNativeModalScan(async()=>{calls++;return {stdout:''};}),/no result after 3 scans/);assert.equal(calls,3); +}); +test('invalid native scan is never interpreted as clean',async()=>{ + await assert.rejects(readNativeModalScan(async()=>({stdout:'{}'})),/Invalid native/); + await assert.rejects(readNativeModalScan(async()=>({stdout:'[null]'})),/Invalid native/); +}); -- 2.51.2