diff --git a/packages/platform-apple/src/runner/__tests__/runner-exchange.test.ts b/packages/platform-apple/src/runner/__tests__/runner-exchange.test.ts index d4e53a3c5a..b5b4b31e9d 100644 --- a/packages/platform-apple/src/runner/__tests__/runner-exchange.test.ts +++ b/packages/platform-apple/src/runner/__tests__/runner-exchange.test.ts @@ -26,6 +26,7 @@ import { let server: FakeRunnerServer | undefined; afterEach(async () => { + vi.unstubAllGlobals(); await server?.close(); server = undefined; }); @@ -408,6 +409,39 @@ test('a drain whose status read fails stops at once and leaves the charge abando assert.equal(session.commandCharges.hasAbandonedCharges, true); }); +test('a read-only command canceled in the connect retry delay withdraws its charge', async () => { + const session = sessionFor(await closedLocalPort()); + session.state = 'starting'; + const controller = new AbortController(); + const refusedAttempts: string[] = []; + vi.stubGlobal( + 'fetch', + vi.fn(async (url: string) => { + refusedAttempts.push(url); + setTimeout(() => controller.abort(createRequestCanceledError()), 0); + throw Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' }); + }), + ); + + await assert.rejects( + executeRunnerExchange( + IOS_SIMULATOR, + session, + { command: 'snapshot', appBundleId: 'com.example.app' }, + undefined, + 10_000, + async () => {}, + controller.signal, + ), + (error: unknown) => + isRequestCanceledError(error) && + (error as { details?: { dispatched?: string } }).details?.dispatched === 'no', + ); + + assert.equal(refusedAttempts.length, 1); + assert.equal(session.commandCharges.hasOutstandingCharges, false); +}); + test('a refused connection withdraws its charge although nothing canceled it', async () => { const port = await closedLocalPort(); const session = sessionFor(port); diff --git a/packages/platform-apple/src/runner/runner-startup-transport.ts b/packages/platform-apple/src/runner/runner-startup-transport.ts index f1aa7c4c52..3c156ca640 100644 --- a/packages/platform-apple/src/runner/runner-startup-transport.ts +++ b/packages/platform-apple/src/runner/runner-startup-transport.ts @@ -79,6 +79,9 @@ export async function waitForRunner( // Every attempt posts the command itself, so the loop gives up "before send" only when no // attempt could have written it. let commandMayHaveBeenWritten = false; + const recordAttemptWriteEvidence = (error: unknown): void => { + if (!isRunnerCommandProvablyUnwritten(error)) commandMayHaveBeenWritten = true; + }; const maxAttempts = Math.max(1, Math.ceil(timeoutMs / RUNNER_CONNECT_ATTEMPT_INTERVAL_MS)); try { return await retryWithPolicy( @@ -100,12 +103,10 @@ export async function waitForRunner( }, setLastError: (err) => { lastError = err; - if (!isRunnerCommandProvablyUnwritten(err)) commandMayHaveBeenWritten = true; + recordAttemptWriteEvidence(err); }, }).catch((error: unknown) => { - if (isRequestCanceledError(error) && !isRunnerCommandProvablyUnwritten(error)) { - commandMayHaveBeenWritten = true; - } + if (isRequestCanceledError(error)) recordAttemptWriteEvidence(error); throw error; }); if (response) return response;