From 8644034c5308b1b40afb2423121d7609b2854e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 12 Jun 2026 20:22:20 +0200 Subject: [PATCH 1/2] fix: relax Android snapshot helper session timeout --- .../__tests__/snapshot-helper-session.test.ts | 58 +++++++++++++------ .../android/snapshot-helper-session.ts | 6 +- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/platforms/android/__tests__/snapshot-helper-session.test.ts b/src/platforms/android/__tests__/snapshot-helper-session.test.ts index 5e44b80ec..cb5828b79 100644 --- a/src/platforms/android/__tests__/snapshot-helper-session.test.ts +++ b/src/platforms/android/__tests__/snapshot-helper-session.test.ts @@ -113,6 +113,23 @@ test('starts and reuses a persistent Android snapshot helper session', async () ); }); +test('allows a persistent session snapshot to use the helper command budget', async () => { + const calls: string[][] = []; + const provider = createSessionProvider({ calls, responseDelayMs: 3_200 }); + + const output = await captureAndroidSnapshotWithHelperSession({ + adb: provider.exec, + adbProvider: provider, + deviceKey: 'android:emulator-5554', + timeoutMs: 10, + commandTimeoutMs: 4_000, + }); + + assert.match(output?.xml ?? '', /snapshot 1/); + assert.equal(output?.metadata.transport, 'persistent-session'); + assert.equal(output?.metadata.sessionReused, false); +}); + test('restarts the helper session when capture options change', async () => { const calls: string[][] = []; const spawnArgs: string[][] = []; @@ -165,6 +182,7 @@ function createSessionProvider(options: { calls: string[][]; spawnArgs?: string[][]; responseMode?: 'ok' | 'malformed'; + responseDelayMs?: number; }): AndroidAdbProvider { return { exec: async (args) => { @@ -191,25 +209,27 @@ function createSessionProvider(options: { } snapshotCount += 1; const body = ``; - socket.end( - sessionResponse({ - requestId, - body, - metadata: { - waitForIdleTimeoutMs: '25', - waitForIdleQuietMs: '25', - timeoutMs: '5000', - maxDepth: '128', - maxNodes: '5000', - rootPresent: 'true', - captureMode: 'interactive-windows', - windowCount: '1', - nodeCount: '1', - truncated: 'false', - elapsedMs: '7', - }, - }), - ); + setTimeout(() => { + socket.end( + sessionResponse({ + requestId, + body, + metadata: { + waitForIdleTimeoutMs: '25', + waitForIdleQuietMs: '25', + timeoutMs: '5000', + maxDepth: '128', + maxNodes: '5000', + rootPresent: 'true', + captureMode: 'interactive-windows', + windowCount: '1', + nodeCount: '1', + truncated: 'false', + elapsedMs: '7', + }, + }), + ); + }, options.responseDelayMs ?? 0); }); }); server.listen(port, '127.0.0.1', () => { diff --git a/src/platforms/android/snapshot-helper-session.ts b/src/platforms/android/snapshot-helper-session.ts index 4398da520..d7c6e588e 100644 --- a/src/platforms/android/snapshot-helper-session.ts +++ b/src/platforms/android/snapshot-helper-session.ts @@ -21,6 +21,7 @@ import { const SESSION_READY_TIMEOUT_MS = 10_000; const SESSION_STOP_TIMEOUT_MS = 1_000; const SESSION_PROCESS_EXIT_TIMEOUT_MS = 2_000; +const SESSION_REQUEST_OVERHEAD_MS = 10_000; const FORWARD_TIMEOUT_MS = 5_000; type AndroidSnapshotHelperSession = { @@ -248,7 +249,10 @@ async function requestSessionSnapshot( resolved: AndroidSnapshotHelperResolvedCaptureOptions, ): Promise { const requestId = `snapshot-${Date.now()}-${Math.random().toString(16).slice(2)}`; - const timeoutMs = Math.max(resolved.timeoutMs + 2_000, 3_000); + const timeoutMs = Math.min( + resolved.commandTimeoutMs, + Math.max(resolved.timeoutMs + SESSION_REQUEST_OVERHEAD_MS, 3_000), + ); const response = await sendSessionCommand(session, `snapshot ${requestId}`, timeoutMs); return parseSessionSnapshotResponse(response, requestId); } From c42366a557b89c6eaecbbdcf4681495c037d9e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Fri, 12 Jun 2026 20:39:34 +0200 Subject: [PATCH 2/2] test: cover Android helper session timeout cap --- .../__tests__/snapshot-helper-session.test.ts | 25 +++++++++++++++++++ .../android/snapshot-helper-session.ts | 2 ++ 2 files changed, 27 insertions(+) diff --git a/src/platforms/android/__tests__/snapshot-helper-session.test.ts b/src/platforms/android/__tests__/snapshot-helper-session.test.ts index cb5828b79..c5a898846 100644 --- a/src/platforms/android/__tests__/snapshot-helper-session.test.ts +++ b/src/platforms/android/__tests__/snapshot-helper-session.test.ts @@ -115,6 +115,7 @@ test('starts and reuses a persistent Android snapshot helper session', async () test('allows a persistent session snapshot to use the helper command budget', async () => { const calls: string[][] = []; + // The delay must stay above the previous 3s session cap to guard the regression. const provider = createSessionProvider({ calls, responseDelayMs: 3_200 }); const output = await captureAndroidSnapshotWithHelperSession({ @@ -130,6 +131,30 @@ test('allows a persistent session snapshot to use the helper command budget', as assert.equal(output?.metadata.sessionReused, false); }); +test('caps a persistent session snapshot at the helper command budget', async () => { + const calls: string[][] = []; + const provider = createSessionProvider({ calls, responseDelayMs: 50 }); + + await assert.rejects( + () => + captureAndroidSnapshotWithHelperSession({ + adb: provider.exec, + adbProvider: provider, + deviceKey: 'android:emulator-5554', + timeoutMs: 10, + commandTimeoutMs: 20, + }), + (error) => { + assert.equal((error as Error).message, 'Android snapshot helper session request timed out'); + const details = (error as { details?: Record }).details; + assert.equal(details?.timeoutMs, 20); + assert.match(String(details?.command), /^snapshot snapshot-/); + assert.equal(typeof details?.port, 'number'); + return true; + }, + ); +}); + test('restarts the helper session when capture options change', async () => { const calls: string[][] = []; const spawnArgs: string[][] = []; diff --git a/src/platforms/android/snapshot-helper-session.ts b/src/platforms/android/snapshot-helper-session.ts index d7c6e588e..a3b53d197 100644 --- a/src/platforms/android/snapshot-helper-session.ts +++ b/src/platforms/android/snapshot-helper-session.ts @@ -249,6 +249,8 @@ async function requestSessionSnapshot( resolved: AndroidSnapshotHelperResolvedCaptureOptions, ): Promise { const requestId = `snapshot-${Date.now()}-${Math.random().toString(16).slice(2)}`; + // Keep the session request generous enough for slow UIAutomator captures, but never + // beyond the command budget the caller already assigned to this snapshot. const timeoutMs = Math.min( resolved.commandTimeoutMs, Math.max(resolved.timeoutMs + SESSION_REQUEST_OVERHEAD_MS, 3_000),