@@ -1507,13 +1507,13 @@ describe('browserToolWatchdogMs', () => {
15071507 } )
15081508
15091509 it . each ( [
1510- [ 'number' , 30_000 , 35_000 ] ,
1511- [ 'numeric string' , '30000' , 35_000 ] ,
1512- [ 'absent' , undefined , 15_000 ] ,
1513- [ 'non-numeric' , 'soon' , 15_000 ] ,
1514- [ 'zero' , 0 , 15_000 ] ,
1515- [ 'negative' , - 5_000 , 15_000 ] ,
1516- [ 'above the wait clamp' , 500_000 , 125_000 ] ,
1510+ [ 'number' , 30_000 , 55_000 ] ,
1511+ [ 'numeric string' , '30000' , 55_000 ] ,
1512+ [ 'absent' , undefined , 35_000 ] ,
1513+ [ 'non-numeric' , 'soon' , 35_000 ] ,
1514+ [ 'zero' , 0 , 35_000 ] ,
1515+ [ 'negative' , - 5_000 , 35_000 ] ,
1516+ [ 'above the wait clamp' , 500_000 , 145_000 ] ,
15171517 ] ) ( 'normalizes browser_wait_for timeout (%s)' , ( _label , timeoutMs , expected ) => {
15181518 const params = timeoutMs === undefined ? { } : { timeoutMs }
15191519
@@ -3098,40 +3098,180 @@ describe('credential protection', () => {
30983098 } )
30993099 } )
31003100
3101+ it ( 'gives a wait that runs to its timeout a full snapshot budget for its closing observation' , async ( ) => {
3102+ const contents = await openPage ( )
3103+ let finishSnapshot : ( value : unknown ) => void = ( ) => { }
3104+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
3105+ if ( ! isPageCall ( expression , 'collectSnapshot' ) ) return Promise . resolve ( undefined )
3106+ return new Promise ( ( resolve ) => {
3107+ finishSnapshot = resolve
3108+ } )
3109+ } )
3110+ vi . useFakeTimers ( )
3111+ try {
3112+ const waiting = driver . executeTool ( 'chat-test' , 'browser_wait_for' , { timeoutMs : 1_000 } )
3113+ await vi . advanceTimersByTimeAsync ( 9_000 )
3114+ finishSnapshot ( {
3115+ url : 'https://example.com/' ,
3116+ title : 'Example' ,
3117+ outline : '- heading "Order confirmed"' ,
3118+ truncated : false ,
3119+ refIds : [ ] ,
3120+ refLineIndexes : { } ,
3121+ nextElementId : 1_000 ,
3122+ } )
3123+ await vi . advanceTimersByTimeAsync ( 1_000 )
3124+
3125+ expect ( await waiting ) . toMatchObject ( {
3126+ ok : true ,
3127+ result : { observation : { ok : true , result : { outline : '- heading "Order confirmed"' } } } ,
3128+ } )
3129+ } finally {
3130+ vi . useRealTimers ( )
3131+ }
3132+ } )
3133+
3134+ it ( 'accepts a later snapshot that keeps refs this scope issued for the same page' , async ( ) => {
3135+ const contents = await openPage ( )
3136+ const snapshot = ( refIds : number [ ] , nextElementId : number ) => ( {
3137+ url : 'https://example.com/' ,
3138+ title : 'Example' ,
3139+ outline : refIds . map ( ( ref ) => `- button "B${ ref } " [ref=${ ref } ]` ) . join ( '\n' ) ,
3140+ truncated : false ,
3141+ refIds,
3142+ refLineIndexes : Object . fromEntries ( refIds . map ( ( ref , index ) => [ ref , index ] ) ) ,
3143+ nextElementId,
3144+ } )
3145+ respondWith ( contents , { collectSnapshot : snapshot ( [ 4 , 5 ] , 6 ) } )
3146+ await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } )
3147+ respondWith ( contents , { collectSnapshot : snapshot ( [ 4 , 6 ] , 7 ) } )
3148+
3149+ const kept = await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } )
3150+ expect ( kept ) . toMatchObject ( { ok : true } )
3151+ respondWith ( contents , { collectSnapshot : snapshot ( [ 2 ] , 7 ) } )
3152+
3153+ const forged = await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } )
3154+ expect ( forged ) . toMatchObject ( { ok : false } )
3155+ } )
3156+
3157+ it ( "forgets a page's oldest refs once its history passes the retention bound" , async ( ) => {
3158+ const contents = await openPage ( )
3159+ const snapshot = ( refIds : number [ ] , nextElementId : number ) => ( {
3160+ url : 'https://example.com/' ,
3161+ title : 'Example' ,
3162+ outline : refIds . map ( ( ref ) => `- button "B${ ref } " [ref=${ ref } ]` ) . join ( '\n' ) ,
3163+ truncated : false ,
3164+ refIds,
3165+ refLineIndexes : Object . fromEntries ( refIds . map ( ( ref , index ) => [ ref , index ] ) ) ,
3166+ nextElementId,
3167+ } )
3168+ let next = 0
3169+ for ( let round = 0 ; round < 19 ; round ++ ) {
3170+ const refIds = Array . from ( { length : 300 } , ( _ , index ) => next + index )
3171+ next += 300
3172+ respondWith ( contents , { collectSnapshot : snapshot ( refIds , next ) } )
3173+ expect ( await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } ) ) . toMatchObject ( {
3174+ ok : true ,
3175+ } )
3176+ }
3177+ respondWith ( contents , { collectSnapshot : snapshot ( [ next - 1 ] , next ) } )
3178+ expect ( await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } ) ) . toMatchObject ( {
3179+ ok : true ,
3180+ } )
3181+ respondWith ( contents , { collectSnapshot : snapshot ( [ 0 ] , next ) } )
3182+
3183+ expect ( await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } ) ) . toMatchObject ( {
3184+ ok : false ,
3185+ } )
3186+ } )
3187+
3188+ it ( 'recaptures with fresh ids when the page reuses an id this scope never committed' , async ( ) => {
3189+ const contents = await openPage ( )
3190+ const snapshot = ( refIds : number [ ] , nextElementId : number ) => ( {
3191+ url : 'https://example.com/' ,
3192+ title : 'Example' ,
3193+ outline : refIds . map ( ( ref ) => `- button "B${ ref } " [ref=${ ref } ]` ) . join ( '\n' ) ,
3194+ truncated : false ,
3195+ refIds,
3196+ refLineIndexes : Object . fromEntries ( refIds . map ( ( ref , index ) => [ ref , index ] ) ) ,
3197+ nextElementId,
3198+ } )
3199+ respondWith ( contents , { collectSnapshot : snapshot ( [ 4 , 5 ] , 6 ) } )
3200+ await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } )
3201+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
3202+ if ( ! isPageCall ( expression , 'collectSnapshot' ) ) return Promise . resolve ( undefined )
3203+ const forgets = expression . includes ( ',true])' )
3204+ return Promise . resolve ( forgets ? snapshot ( [ 6 , 7 ] , 8 ) : snapshot ( [ 2 , 6 ] , 7 ) )
3205+ } )
3206+
3207+ const result = await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } )
3208+
3209+ expect ( result ) . toMatchObject ( {
3210+ ok : true ,
3211+ result : { outline : expect . stringContaining ( '[ref=7]' ) } ,
3212+ } )
3213+ } )
3214+
3215+ it ( 'acts on a ref an earlier snapshot of the same page issued' , async ( ) => {
3216+ const contents = await openPage ( )
3217+ respondWith ( contents , {
3218+ collectSnapshot : {
3219+ url : 'https://example.com/' ,
3220+ title : 'Example' ,
3221+ outline : '- button "Pins" [ref=4]\n- button "Files" [ref=5]' ,
3222+ truncated : false ,
3223+ refIds : [ 4 , 5 ] ,
3224+ refLineIndexes : { 4 : 0 , 5 : 1 } ,
3225+ nextElementId : 6 ,
3226+ } ,
3227+ } )
3228+ await driver . executeTool ( 'chat-test' , 'browser_snapshot' , { } )
3229+ respondWith ( contents , {
3230+ collectSnapshot : {
3231+ url : 'https://example.com/' ,
3232+ title : 'Example' ,
3233+ outline : '- dialog "Pick a time" [ref=6]' ,
3234+ truncated : false ,
3235+ refIds : [ 6 ] ,
3236+ refLineIndexes : { 6 : 0 } ,
3237+ nextElementId : 7 ,
3238+ } ,
3239+ } )
3240+ await driver . executeTool ( 'chat-test' , 'browser_find' , { query : 'time' } )
3241+
3242+ const clicked = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 4 } )
3243+ expect ( clicked ) . toMatchObject ( { ok : true , result : { dispatched : true } } )
3244+ } )
3245+
31013246 it . each ( [
31023247 [ 'browser_snapshot' , 'true' ] ,
31033248 [ 'browser_find' , 'false' ] ,
3104- ] as const ) (
3105- 'passes the current root ref to %s and invalidates previous refs' ,
3106- async ( tool , markNew ) => {
3107- const contents = await openPage ( )
3108- respondWith ( contents , {
3109- collectSnapshot : {
3110- url : 'https://example.com/login' ,
3111- title : 'Scoped' ,
3112- scoped : true ,
3113- outline : '- button "Save" [ref=1]' ,
3114- truncated : false ,
3115- refIds : [ 1 ] ,
3116- refLineIndexes : { 1 : 0 } ,
3117- nextElementId : 2 ,
3118- } ,
3119- } )
3120- const result = await driver . executeTool ( 'chat-test' , tool , { elementId : 0 , query : 'Save' } )
3121- expect ( result ) . toMatchObject ( { ok : true , result : { scoped : true } } )
3122- expect (
3123- vi
3124- . mocked ( contents . executeJavaScript )
3125- . mock . calls . some (
3126- ( [ expression ] ) =>
3127- isPageCall ( expression , 'collectSnapshot' ) &&
3128- expression . includes ( `.apply(null, [1,0,${ markNew } ])` )
3129- )
3130- ) . toBe ( true )
3131- const stale = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 0 } )
3132- expect ( stale ) . toMatchObject ( { ok : false } )
3133- }
3134- )
3249+ ] as const ) ( 'passes the current root ref to %s' , async ( tool , markNew ) => {
3250+ const contents = await openPage ( )
3251+ respondWith ( contents , {
3252+ collectSnapshot : {
3253+ url : 'https://example.com/login' ,
3254+ title : 'Scoped' ,
3255+ scoped : true ,
3256+ outline : '- button "Save" [ref=1]' ,
3257+ truncated : false ,
3258+ refIds : [ 1 ] ,
3259+ refLineIndexes : { 1 : 0 } ,
3260+ nextElementId : 2 ,
3261+ } ,
3262+ } )
3263+ const result = await driver . executeTool ( 'chat-test' , tool , { elementId : 0 , query : 'Save' } )
3264+ expect ( result ) . toMatchObject ( { ok : true , result : { scoped : true } } )
3265+ expect (
3266+ vi
3267+ . mocked ( contents . executeJavaScript )
3268+ . mock . calls . some (
3269+ ( [ expression ] ) =>
3270+ isPageCall ( expression , 'collectSnapshot' ) &&
3271+ expression . includes ( `.apply(null, [1,0,${ markNew } ,` )
3272+ )
3273+ ) . toBe ( true )
3274+ } )
31353275
31363276 it . each ( [
31373277 { kind : 'input:checkbox' , checked : true , disabled : false , readOnly : false } ,
0 commit comments