From a3147b2ef64711777c9e05f6715281814734d6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 12 Jun 2026 20:53:57 +0200 Subject: [PATCH] fix: tighten ios runner crash classification --- .../ios/__tests__/runner-client.test.ts | 58 +++++++++++++++++++ .../ios/runner-failure-diagnostics.ts | 3 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/platforms/ios/__tests__/runner-client.test.ts b/src/platforms/ios/__tests__/runner-client.test.ts index 8e53ed948..6ddc3ff1b 100644 --- a/src/platforms/ios/__tests__/runner-client.test.ts +++ b/src/platforms/ios/__tests__/runner-client.test.ts @@ -720,6 +720,64 @@ Thread 0 Crashed:: Dispatch queue: com.apple.main-thread ); }); +test('parseRunnerResponse classifies explicit target app crashes from runner log tail', async () => { + const logPath = writeRunnerLogTail(` +AGENT_DEVICE_RUNNER_COMMAND_FAILED command=snapshot +The application under test terminated unexpectedly. +`); + const response = new Response( + JSON.stringify({ + ok: false, + error: { + code: 'COMMAND_FAILED', + message: 'Runner error', + }, + }), + ); + const session = { ready: true }; + + await assert.rejects( + () => parseRunnerResponse(response, session, logPath), + (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.code, 'IOS_TARGET_APP_CRASH'); + assert.equal(error.details?.runnerFailureReason, 'target_app_crash'); + assert.match(String(error.details?.hint), /target iOS app appears to have crashed/); + assert.equal(isRetryableRunnerError(error), false); + return true; + }, + ); +}); + +test('parseRunnerResponse does not classify incidental XCTest crash text as target app crash', async () => { + const logPath = writeRunnerLogTail(` +XCTest runner recovered from a previous test note: the word crashed appeared in debug output. +AGENT_DEVICE_RUNNER_COMMAND_FAILED command=snapshot error=fetch failed +`); + const response = new Response( + JSON.stringify({ + ok: false, + error: { + code: 'COMMAND_FAILED', + message: 'fetch failed', + }, + }), + ); + const session = { ready: true }; + + await assert.rejects( + () => parseRunnerResponse(response, session, logPath), + (error: unknown) => { + assert.ok(error instanceof AppError); + assert.equal(error.code, 'COMMAND_FAILED'); + assert.equal(error.details?.runnerFailureReason, undefined); + assert.equal(error.details?.hint, undefined); + assert.equal(isRetryableRunnerError(error), true); + return true; + }, + ); +}); + test('parseRunnerResponse keeps ordinary runner failures generic without crash log evidence', async () => { const logPath = writeRunnerLogTail( 'AGENT_DEVICE_RUNNER_COMMAND_FAILED command=type error=main thread execution timed out', diff --git a/src/platforms/ios/runner-failure-diagnostics.ts b/src/platforms/ios/runner-failure-diagnostics.ts index f0002b123..07378a542 100644 --- a/src/platforms/ios/runner-failure-diagnostics.ts +++ b/src/platforms/ios/runner-failure-diagnostics.ts @@ -82,8 +82,7 @@ function isTargetAppCrash(normalized: string): boolean { normalized.includes('process crashed') || normalized.includes('the application under test') || normalized.includes('terminated unexpectedly') || - (normalized.includes('exception type:') && normalized.includes('thread 0 crashed')) || - (normalized.includes('crashed') && normalized.includes('xctest')) + (normalized.includes('exception type:') && normalized.includes('thread 0 crashed')) ); }