@@ -143,6 +143,28 @@ test('mutating commands restart stale sessions when readiness preflight times ou
143143 assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls [ 1 ] ?. [ 1 ] , freshSession ) ;
144144} ) ;
145145
146+ test ( 'mutating commands emit readiness recovery diagnostics after failed preflight restart succeeds' , async ( ) => {
147+ const staleSession = makeRunnerSession ( { port : 8100 , ready : true } ) ;
148+ const freshSession = makeRunnerSession ( { port : 8101 , ready : false } ) ;
149+
150+ mockEnsureRunnerSession . mockResolvedValueOnce ( staleSession ) . mockResolvedValueOnce ( freshSession ) ;
151+ mockExecuteRunnerCommandWithSession
152+ . mockRejectedValueOnce (
153+ new AppError ( 'COMMAND_FAILED' , 'fetch failed' , {
154+ runnerReadinessPreflightFailed : true ,
155+ } ) ,
156+ )
157+ . mockResolvedValueOnce ( { message : 'tapped' } ) ;
158+
159+ const diagnostics = await captureDiagnostics ( async ( ) => {
160+ const result = await runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ;
161+ assert . deepEqual ( result , { message : 'tapped' } ) ;
162+ } ) ;
163+
164+ assert . match ( diagnostics , / i o s _ r u n n e r _ r e a d i n e s s _ p r e f l i g h t _ r e c o v e r e d / ) ;
165+ assert . match ( diagnostics , / " r e c o v e r y " : " s e s s i o n _ r e s t a r t e d " / ) ;
166+ } ) ;
167+
146168test ( 'mutating commands do not restart or replay after command send failure' , async ( ) => {
147169 const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
148170
@@ -197,6 +219,35 @@ test('mutating commands recover cached responses before invalidating after comma
197219 assert . equal ( statusCommand . statusCommandId , sentCommand . commandId ) ;
198220} ) ;
199221
222+ test ( 'mutating commands run status recovery after transport failure when readiness preflight was skipped' , async ( ) => {
223+ const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
224+
225+ mockEnsureRunnerSession . mockResolvedValueOnce ( session ) ;
226+ mockExecuteRunnerCommandWithSession
227+ . mockRejectedValueOnce (
228+ new AppError ( 'COMMAND_FAILED' , 'fetch failed' , {
229+ runnerReadinessPreflightSkipped : true ,
230+ runnerReadinessPreflightSkipReason : 'recent_successful_response' ,
231+ } ) ,
232+ )
233+ . mockResolvedValueOnce ( {
234+ lifecycleState : 'completed' ,
235+ lifecycleResponseJson : JSON . stringify ( { ok : true , data : { message : 'tapped' } } ) ,
236+ } ) ;
237+
238+ const diagnostics = await captureDiagnostics ( async ( ) => {
239+ const result = await runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ;
240+ assert . deepEqual ( result , { message : 'tapped' } ) ;
241+ } ) ;
242+
243+ assert . equal ( mockInvalidateRunnerSession . mock . calls . length , 0 ) ;
244+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls . length , 2 ) ;
245+ assert . equal ( mockExecuteRunnerCommandWithSession . mock . calls [ 1 ] ?. [ 2 ] . command , 'status' ) ;
246+ assert . match ( diagnostics , / i o s _ r u n n e r _ c o m m a n d _ s t a t u s _ r e c o v e r y / ) ;
247+ assert . match ( diagnostics , / " r e a d i n e s s P r e f l i g h t S k i p p e d " : t r u e / ) ;
248+ assert . match ( diagnostics , / " r e a d i n e s s P r e f l i g h t S k i p R e a s o n " : " r e c e n t _ s u c c e s s f u l _ r e s p o n s e " / ) ;
249+ } ) ;
250+
200251test ( 'mutating commands keep invalidating when status cannot find the command' , async ( ) => {
201252 const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
202253
@@ -348,6 +399,36 @@ test('mutating commands report recovery guidance when completed status has no re
348399 } ) ;
349400} ) ;
350401
402+ test ( 'mutating commands include skipped readiness context in lost-response guidance' , async ( ) => {
403+ const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
404+
405+ mockEnsureRunnerSession . mockResolvedValueOnce ( session ) ;
406+ mockExecuteRunnerCommandWithSession
407+ . mockRejectedValueOnce (
408+ new AppError ( 'COMMAND_FAILED' , 'fetch failed' , {
409+ runnerReadinessPreflightSkipped : true ,
410+ runnerReadinessPreflightSkipReason : 'recent_successful_response' ,
411+ runnerReadinessPreflightSkippedAgeMs : 4 ,
412+ } ) ,
413+ )
414+ . mockResolvedValueOnce ( { lifecycleState : 'completed' } ) ;
415+
416+ await assert . rejects (
417+ ( ) => runIosRunnerCommand ( IOS_SIMULATOR , { command : 'tap' , x : 120 , y : 240 } ) ,
418+ ( error : unknown ) => {
419+ assert . ok ( error instanceof AppError ) ;
420+ assert . equal ( error . details ?. recovery , 'completed_without_retained_response' ) ;
421+ assert . equal ( error . details ?. readinessPreflightSkipped , true ) ;
422+ assert . equal ( error . details ?. readinessPreflightSkipReason , 'recent_successful_response' ) ;
423+ assert . equal ( error . details ?. readinessPreflightSkippedAgeMs , 4 ) ;
424+ assert . match ( String ( error . details ?. hint ) , / s k i p p e d t h e u p t i m e p r e f l i g h t / ) ;
425+ assert . match ( String ( error . details ?. hint ) , / s t a t u s r e c o v e r y c o n f i r m e d / ) ;
426+ assert . match ( String ( error . details ?. hint ) , / s n a p s h o t - i / ) ;
427+ return true ;
428+ } ,
429+ ) ;
430+ } ) ;
431+
351432test ( 'mutating commands preserve runner failure details from status recovery' , async ( ) => {
352433 const session = makeRunnerSession ( { port : 8100 , ready : true } ) ;
353434
@@ -502,3 +583,8 @@ function makeRunnerSession(overrides: Partial<RunnerSession> = {}): RunnerSessio
502583 ...overrides ,
503584 } as RunnerSession ;
504585}
586+
587+ async function captureDiagnostics ( callback : ( ) => Promise < void > ) : Promise < string > {
588+ await callback ( ) ;
589+ return JSON . stringify ( mockEmitDiagnostic . mock . calls . map ( ( [ event ] ) => event ) ) ;
590+ }
0 commit comments