@@ -164,64 +164,80 @@ describe('native computer completion during page unload', () => {
164164 }
165165 } )
166166
167- it ( 'retains confirmed zero-dispatch failure after all delivery paths fail and releases it after replay acknowledgment' , async ( ) => {
168- native . execute . mockRejectedValueOnce (
169- new ComputerUseError ( {
170- code : 'activation_required' ,
171- message : 'Activate first.' ,
172- dispatchState : 'not_started' ,
167+ it . each ( [ false , true ] ) (
168+ 'retains native input uncertainty through delivery failure and replay (unknown=%s)' ,
169+ async ( outcomeUnknown ) => {
170+ native . execute . mockRejectedValueOnce (
171+ new ComputerUseError ( {
172+ code : outcomeUnknown ? 'focus_changed' : 'activation_required' ,
173+ message : outcomeUnknown
174+ ? 'The exact editor is no longer focused; remaining input was not dispatched. Observe again.'
175+ : 'Activate first.' ,
176+ ...( outcomeUnknown ? { } : { dispatchState : 'not_started' as const } ) ,
177+ } )
178+ )
179+ const fetch = vi
180+ . fn < typeof globalThis . fetch > ( )
181+ . mockResolvedValue ( new Response ( null , { status : 503 } ) )
182+ vi . stubGlobal ( 'fetch' , fetch )
183+ const bodies : NodeBlob [ ] = [ ]
184+ Object . defineProperty ( navigator , 'sendBeacon' , {
185+ configurable : true ,
186+ value : ( _url : string , body : NodeBlob ) => {
187+ bodies . push ( body )
188+ return false
189+ } ,
173190 } )
174- )
175- const fetch = vi
176- . fn < typeof globalThis . fetch > ( )
177- . mockResolvedValue ( new Response ( null , { status : 503 } ) )
178- vi . stubGlobal ( 'fetch' , fetch )
179- const bodies : NodeBlob [ ] = [ ]
180- Object . defineProperty ( navigator , 'sendBeacon' , {
181- configurable : true ,
182- value : ( _url : string , body : NodeBlob ) => {
183- bodies . push ( body )
184- return false
185- } ,
186- } )
187- const id = nextId ( )
188- const timestamp = new Date ( ) . toISOString ( )
189- const execution = executeComputerToolOnClient ( id , { action : 'list_apps' } , timestamp )
190- await vi . runAllTimersAsync ( )
191- await execution
192- try {
191+ const id = nextId ( )
192+ const timestamp = new Date ( ) . toISOString ( )
193+ const input = {
194+ action : 'type_text' as const ,
195+ bundleId : 'com.example.Fixture' ,
196+ snapshotId : 'before' ,
197+ elementId : 'editor' ,
198+ text : 'prefix then interruption' ,
199+ }
200+ const execution = executeComputerToolOnClient ( id , input , timestamp )
201+ await vi . runAllTimersAsync ( )
202+ await execution
203+ try {
204+ window . dispatchEvent ( new Event ( 'pagehide' ) )
205+ await flushMicrotasks ( )
206+ expect ( bodies ) . toHaveLength ( 1 )
207+ expect ( copilotConfirmBodySchema . parse ( JSON . parse ( await bodies [ 0 ] . text ( ) ) ) ) . toMatchObject ( {
208+ toolCallId : id ,
209+ status : 'error' ,
210+ data : {
211+ code : outcomeUnknown ? 'focus_changed' : 'activation_required' ,
212+ ...( outcomeUnknown ? { } : { dispatchState : 'not_started' } ) ,
213+ doNotRetry : outcomeUnknown ,
214+ outcomeUnknown,
215+ } ,
216+ } )
217+ const fallback = fetch . mock . calls . find ( ( [ , init ] ) => init ?. keepalive )
218+ expect ( fallback ?. [ 1 ] ?. body ) . toBe ( await bodies [ 0 ] . text ( ) )
219+ expect ( native . cancel ) . not . toHaveBeenCalled ( )
220+ } finally {
221+ fetch . mockResolvedValue ( new Response ( null , { status : 200 } ) )
222+ await executeComputerToolOnClient ( id , input , timestamp )
223+ }
224+ const count = bodies . length
193225 window . dispatchEvent ( new Event ( 'pagehide' ) )
194- await flushMicrotasks ( )
195- expect ( bodies ) . toHaveLength ( 1 )
196- expect ( copilotConfirmBodySchema . parse ( JSON . parse ( await bodies [ 0 ] . text ( ) ) ) ) . toMatchObject ( {
226+ expect ( bodies ) . toHaveLength ( count )
227+ expect (
228+ copilotConfirmBodySchema . parse ( JSON . parse ( String ( fetch . mock . lastCall ?. [ 1 ] ?. body ) ) )
229+ ) . toMatchObject ( {
197230 toolCallId : id ,
198231 status : 'error' ,
199232 data : {
200- code : 'activation_required' ,
201- dispatchState : 'not_started' ,
202- doNotRetry : false ,
203- outcomeUnknown : false ,
233+ ...( outcomeUnknown ? { } : { dispatchState : 'not_started' } ) ,
234+ outcomeUnknown,
235+ doNotRetry : outcomeUnknown ,
204236 } ,
205237 } )
206- const fallback = fetch . mock . calls . find ( ( [ , init ] ) => init ?. keepalive )
207- expect ( fallback ?. [ 1 ] ?. body ) . toBe ( await bodies [ 0 ] . text ( ) )
208- expect ( native . cancel ) . not . toHaveBeenCalled ( )
209- } finally {
210- fetch . mockResolvedValue ( new Response ( null , { status : 200 } ) )
211- await executeComputerToolOnClient ( id , { action : 'list_apps' } , timestamp )
238+ expect ( native . execute ) . toHaveBeenCalledOnce ( )
212239 }
213- const count = bodies . length
214- window . dispatchEvent ( new Event ( 'pagehide' ) )
215- expect ( bodies ) . toHaveLength ( count )
216- expect (
217- copilotConfirmBodySchema . parse ( JSON . parse ( String ( fetch . mock . lastCall ?. [ 1 ] ?. body ) ) )
218- ) . toMatchObject ( {
219- toolCallId : id ,
220- status : 'error' ,
221- data : { dispatchState : 'not_started' , outcomeUnknown : false } ,
222- } )
223- expect ( native . execute ) . toHaveBeenCalledOnce ( )
224- } )
240+ )
225241
226242 it ( 'reports an uncertain action and cancels only while native work is still pending' , async ( ) => {
227243 const nativeResult = createDeferred < ComputerUseResult > ( )
0 commit comments