fix(android): stop a structurally-failed helper session before the one-shot viewport retry

A structured ok=false viewport response leaves the session process alive, and
Android permits only one instrumentation owner of UiAutomation - running the
one-shot fallback against a still-live helper contends with it and masks the
original structured failure. Stop the session first; regression pins that the
one-shot retry only executes once the session is gone.
This commit is contained in:
Michał Pierzchała
2026-07-16 11:30:48 +02:00
parent 0b033f19db
commit 5961b9247c
2 changed files with 50 additions and 1 deletions
@@ -672,3 +672,48 @@ test('viewport falls back to one-shot instrumentation after a session error', as
assert.ok(oneShotArgs?.includes('viewport'));
assert.equal(await session.isSessionAlive(), false);
});
test('a structured ok=false viewport response stops the session before the one-shot retry', async () => {
const device = makeIsolatedDevice();
const session = await startFakeTouchHelperSession(device, (command, requestId) => {
if (command.startsWith('viewport')) {
return sessionHeaderResponse({
agentDeviceProtocol: 'android-snapshot-helper-v1',
requestId,
ok: 'false',
errorType: 'java.lang.IllegalStateException',
message: 'Active application interaction viewport is unavailable',
});
}
return sessionHeaderResponse({
agentDeviceProtocol: 'android-snapshot-helper-v1',
requestId,
ok: 'true',
});
});
let oneShotArgs: string[] | undefined;
const viewportResult = await withAndroidAdbProvider(
{
exec: currentVersionAdb(async (args) => {
// One instrumentation may own UiAutomation: the one-shot retry must only run once the
// structurally-failed session has been stopped.
assert.equal(await session.isSessionAlive(), false);
oneShotArgs = args;
return {
exitCode: 0,
stdout: [
resultRecord({ ok: 'true', x: '5', y: '6', width: '300', height: '400' }),
'INSTRUMENTATION_CODE: 0',
].join('\n'),
stderr: '',
};
}),
},
{ serial: device.id },
async () => await readAndroidTouchHelperViewport(device),
);
assert.deepEqual(viewportResult, { x: 5, y: 6, width: 300, height: 400 });
assert.ok(oneShotArgs?.includes('viewport'));
});
+5 -1
View File
@@ -17,6 +17,7 @@ import { ensureAndroidSnapshotHelper } from './snapshot-helper-install.ts';
import {
getAndroidSnapshotHelperSessionDeviceKey,
runAndroidSnapshotHelperSessionTouchCommand,
stopAndroidSnapshotHelperSession,
} from './snapshot-helper-session.ts';
import {
ANDROID_SNAPSHOT_HELPER_PROTOCOL,
@@ -112,7 +113,10 @@ export async function readAndroidTouchHelperViewport(device: DeviceInfo): Promis
});
if (sessionHeaders) return readViewportResult(sessionHeaders);
} catch (error) {
// Viewport reads are idempotent; after the failed session is stopped, retry one-shot below.
// Viewport reads are idempotent, so a fresh one-shot run may still answer. The session must
// be stopped first: Android permits one instrumentation owner of UiAutomation, and a helper
// left alive after a structured failure would contend with the one-shot instrumentation.
await stopAndroidSnapshotHelperSession(prepared.deviceKey);
emitDiagnostic({
level: 'warn',
phase: 'android_touch_helper_viewport_session_fallback',