@@ -135,9 +135,9 @@ test('mutating commands do not restart or replay after command send failure', as
135135 const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
136136
137137 mockEnsureRunnerSession . mockResolvedValueOnce ( session ) ;
138- mockExecuteRunnerCommandWithSession . mockRejectedValueOnce (
139- new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) ,
140- ) ;
138+ mockExecuteRunnerCommandWithSession
139+ . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) )
140+ . mockResolvedValueOnce ( { lifecycleState : 'notAccepted' } ) ;
141141
142142 await assert . rejects ( ( ) =>
143143 runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ,
@@ -150,7 +150,67 @@ test('mutating commands do not restart or replay after command send failure', as
150150 'transport_error_after_command_send' ,
151151 ] ) ;
152152 assert . equal ( mockStopRunnerSession . mock . calls . length , 0 ) ;
153- assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 1 ) ;
153+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 2 ) ;
154+ } ) ;
155+
156+ test ( 'mutating commands recover cached responses before invalidating after command send failure' , async ( ) => {
157+ const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
158+
159+ mockEnsureRunnerSession . mockResolvedValueOnce ( session ) ;
160+ mockExecuteRunnerCommandWithSession
161+ . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) )
162+ . mockResolvedValueOnce ( {
163+ lifecycleState : 'completed' ,
164+ lifecycleResponseJson : JSON . stringify ( { ok : true , data : { message : 'tapped' } } ) ,
165+ } ) ;
166+
167+ const result = await runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ;
168+
169+ assert . deepEqual ( result , { message : 'tapped' } ) ;
170+ assert . equal ( mockInvalidateRunnerSession . mock . calls . length , 0 ) ;
171+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 2 ) ;
172+ const sentCommand = mockExecuteRunnerCommandWithSession . mock . calls [ 0 ] ?. [ 2 ] ;
173+ const statusCommand = mockExecuteRunnerCommandWithSession . mock . calls [ 1 ] ?. [ 2 ] ;
174+ assert . equal ( statusCommand . command , 'status' ) ;
175+ assert . equal ( statusCommand . statusCommandId , sentCommand . commandId ) ;
176+ } ) ;
177+
178+ test ( 'mutating commands keep invalidating when status cannot find the command' , async ( ) => {
179+ const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
180+
181+ mockEnsureRunnerSession . mockResolvedValueOnce ( session ) ;
182+ mockExecuteRunnerCommandWithSession
183+ . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) )
184+ . mockResolvedValueOnce ( {
185+ lifecycleState : 'notAccepted' ,
186+ } ) ;
187+
188+ await assert . rejects ( ( ) =>
189+ runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ,
190+ ) ;
191+
192+ assert . deepEqual ( mockInvalidateRunnerSession . mock . calls , [
193+ [ session , 'transport_error_after_command_send' ] ,
194+ ] ) ;
195+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 2 ) ;
196+ } ) ;
197+
198+ test ( 'read-only commands retry when completed status has no retained response' , async ( ) => {
199+ const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
200+
201+ mockEnsureRunnerSession . mockResolvedValue ( session ) ;
202+ mockExecuteRunnerCommandWithSession
203+ . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) )
204+ . mockResolvedValueOnce ( { lifecycleState : 'completed' } )
205+ . mockResolvedValueOnce ( { nodes : [ ] , truncated : false } ) ;
206+
207+ const result = await runIosRunnerCommand ( IOS_SIMULATOR , { command : 'snapshot' } ) ;
208+
209+ assert . deepEqual ( result , { nodes : [ ] , truncated : false } ) ;
210+ assert . equal ( mockInvalidateRunnerSession . mock . calls . length , 0 ) ;
211+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 3 ) ;
212+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls [ 1 ] ?. [ 2 ] . command , 'status' ) ;
213+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls [ 2 ] ?. [ 2 ] . command , 'snapshot' ) ;
154214} ) ;
155215
156216test ( 'mutating commands invalidate the retry session without replaying again' , async ( ) => {
@@ -160,7 +220,8 @@ test('mutating commands invalidate the retry session without replaying again', a
160220 mockEnsureRunnerSession . mockResolvedValueOnce ( staleSession ) . mockResolvedValueOnce ( freshSession ) ;
161221 mockExecuteRunnerCommandWithSession
162222 . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'Runner did not accept connection' ) )
163- . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) ) ;
223+ . mockRejectedValueOnce ( new AppError ( 'COMMAND_FAILED' , 'fetch failed' ) )
224+ . mockResolvedValueOnce ( { lifecycleState : 'notAccepted' } ) ;
164225
165226 await assert . rejects ( ( ) =>
166227 runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ,
@@ -171,7 +232,7 @@ test('mutating commands invalidate the retry session without replaying again', a
171232 [ staleSession , 'runner_connect_failed_before_command_send' ] ,
172233 [ freshSession , 'transport_error_after_retry_command_send' ] ,
173234 ] ) ;
174- assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 2 ) ;
235+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 3 ) ;
175236} ) ;
176237
177238function makeRunnerSession ( overrides : Partial < RunnerSession > = { } ) : RunnerSession {
0 commit comments