@@ -2524,7 +2524,7 @@ describe('credential protection', () => {
25242524 observation : {
25252525 ok : false ,
25262526 doNotRetry : true ,
2527- note : expect . stringContaining ( 'The action already ran ' ) ,
2527+ note : expect . stringContaining ( 'The action was dispatched ' ) ,
25282528 } ,
25292529 } ,
25302530 } )
@@ -3892,6 +3892,69 @@ describe('credential protection', () => {
38923892 expect ( release ) . toHaveBeenCalledWith ( contents , input )
38933893 } )
38943894
3895+ it . each ( [ 'cancelled' , 'timed out' ] as const ) (
3896+ 'does not retry a dispatched upload when acknowledgement is %s' ,
3897+ async ( stop ) => {
3898+ const contents = await openPage ( )
3899+ const input = { objectId : 'isolated-input' , multiple : false }
3900+ vi . spyOn ( cdp , 'resolveFileInput' ) . mockResolvedValue ( input )
3901+ stageUploadFiles . mockResolvedValue ( [ '/staged/a.pdf' ] )
3902+ let acknowledge : ( ) => void = ( ) => { }
3903+ const acknowledgement = new Promise < void > ( ( resolve ) => {
3904+ acknowledge = resolve
3905+ } )
3906+ const send = vi . mocked ( contents . debugger . sendCommand )
3907+ send . mockImplementation ( async ( method , params ) => {
3908+ if ( method === 'Runtime.callFunctionOn' ) {
3909+ const mode = ( params ?. arguments as Array < { value : string } > ) [ 0 ] . value
3910+ return mode === 'input'
3911+ ? { result : { objectId : 'original-input' } }
3912+ : { result : { value : { files : [ { name : 'a.pdf' , size : 3 } ] } } }
3913+ }
3914+ if ( method === 'DOM.setFileInputFiles' ) await acknowledgement
3915+ return { }
3916+ } )
3917+ vi . useFakeTimers ( )
3918+ try {
3919+ const pending = driver . executeTool (
3920+ 'chat-test' ,
3921+ 'browser_upload_file' ,
3922+ { elementId : 0 , paths : [ 'files/a.pdf' ] } ,
3923+ 'unacknowledged-upload'
3924+ )
3925+ await vi . advanceTimersByTimeAsync ( 200 )
3926+ expect ( cdpCalls ( contents , 'DOM.setFileInputFiles' ) ) . toHaveLength ( 1 )
3927+ expect ( cdpCalls ( contents , 'Runtime.releaseObject' ) ) . toHaveLength ( 0 )
3928+
3929+ if ( stop === 'cancelled' ) driver . cancelTool ( 'chat-test' , 'unacknowledged-upload' )
3930+ else
3931+ await vi . advanceTimersByTimeAsync (
3932+ driver . browserToolWatchdogMs ( 'browser_upload_file' , { } ) !
3933+ )
3934+
3935+ await expect ( pending ) . resolves . toMatchObject ( {
3936+ ok : true ,
3937+ result : {
3938+ dispatched : true ,
3939+ observation : { ok : false , doNotRetry : true } ,
3940+ } ,
3941+ } )
3942+ await expect (
3943+ driver . executeTool ( 'chat-test' , 'browser_list_tabs' , { } , 'after-unacknowledged-upload' )
3944+ ) . resolves . toMatchObject ( { ok : true } )
3945+ } finally {
3946+ acknowledge ( )
3947+ await vi . advanceTimersByTimeAsync ( 200 )
3948+ vi . useRealTimers ( )
3949+ }
3950+ expect ( cdpCalls ( contents , 'DOM.setFileInputFiles' ) ) . toHaveLength ( 1 )
3951+ expect ( cdpCalls ( contents , 'Runtime.releaseObject' ) . map ( ( [ , params ] ) => params ) ) . toEqual ( [
3952+ { objectId : 'original-input' } ,
3953+ { objectId : 'isolated-input' } ,
3954+ ] )
3955+ }
3956+ )
3957+
38953958 it . each ( [ 'cancelled' , 'timed out' ] as const ) (
38963959 'retains an acknowledged upload when readback is %s and releases its handle when readback settles' ,
38973960 async ( stop ) => {
@@ -3941,7 +4004,7 @@ describe('credential protection', () => {
39414004 observation : {
39424005 ok : false ,
39434006 doNotRetry : true ,
3944- note : expect . stringContaining ( 'The action already ran ' ) ,
4007+ note : expect . stringContaining ( 'The action was dispatched ' ) ,
39454008 } ,
39464009 } ,
39474010 } )
0 commit comments