diff --git a/src/platforms/android/__tests__/snapshot-helper-session.test.ts b/src/platforms/android/__tests__/snapshot-helper-session.test.ts index 5e44b80ec..c5a898846 100644 --- a/src/platforms/android/__tests__/snapshot-helper-session.test.ts +++ b/src/platforms/android/__tests__/snapshot-helper-session.test.ts @@ -113,6 +113,48 @@ 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({ + 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('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[][] = []; @@ -165,6 +207,7 @@ function createSessionProvider(options: { calls: string[][]; spawnArgs?: string[][]; responseMode?: 'ok' | 'malformed'; + responseDelayMs?: number; }): AndroidAdbProvider { return { exec: async (args) => { @@ -191,25 +234,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..a3b53d197 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,12 @@ 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); + // 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), + ); const response = await sendSessionCommand(session, `snapshot ${requestId}`, timeoutMs); return parseSessionSnapshotResponse(response, requestId); }