From f3312d090feb32f321c4b5b0ece7160ff9f5f6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Thu, 15 Oct 2026 13:55:38 +0200 Subject: [PATCH 1/2] feat(sdk): export isUnreadableCaptureContentError for snapshot polling callers --- src/sdk/index.test.ts | 37 +++++++++++++++++++ src/sdk/index.ts | 1 + .../installed-package-metro.test.ts | 1 + website/docs/docs/client-api.md | 3 ++ 4 files changed, 42 insertions(+) create mode 100644 src/sdk/index.test.ts diff --git a/src/sdk/index.test.ts b/src/sdk/index.test.ts new file mode 100644 index 0000000000..ab53d8d706 --- /dev/null +++ b/src/sdk/index.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; +import { ANDROID_CONTENT_RECOVERY_REASONS } from '@agent-device/contracts/android-snapshot-quality'; +import { throwDaemonError } from '@agent-device/kernel/errors'; +import { AppError, isUnreadableCaptureContentError, normalizeAgentDeviceError } from './index.ts'; + +// Public-entry contract, through `./index.ts`: a client polling with `snapshot` classifies the +// error it caught after the daemon wire round trip, not the producer's in-process error. + +function caughtAfterWire(thrown: AppError): unknown { + try { + throwDaemonError(normalizeAgentDeviceError(thrown)); + } catch (error) { + return error; + } + return assert.fail('throwDaemonError returned'); +} + +test('an Android content verdict stays unreadable across the daemon wire', () => { + for (const reason of ANDROID_CONTENT_RECOVERY_REASONS) { + const verdict = new AppError('COMMAND_FAILED', 'insufficient app content', { + androidSnapshotHelperFailureReason: reason, + retriable: true, + }); + + assert.equal(isUnreadableCaptureContentError(caughtAfterWire(verdict)), true, reason); + } +}); + +test('a retriable mechanism failure is not an unreadable capture', () => { + const adbDrop = new AppError('COMMAND_FAILED', 'adb connection dropped', { + reason: 'connection_dropped', + retriable: true, + }); + + assert.equal(isUnreadableCaptureContentError(caughtAfterWire(adbDrop)), false); +}); diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 7556c9c42b..d0f0d738a9 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -6,6 +6,7 @@ export { normalizeAgentDeviceError, } from '@agent-device/kernel/errors'; export { centerOfRect } from '@agent-device/kernel/snapshot'; +export { isUnreadableCaptureContentError } from '@agent-device/contracts/android-snapshot-quality'; // The types the root's two functions are written in. `createAgentDeviceClient` returns // `AgentDeviceClient`, whose every option and result type is a named type of diff --git a/test/integration/installed-package-metro.test.ts b/test/integration/installed-package-metro.test.ts index e251a2b450..0b7b516119 100644 --- a/test/integration/installed-package-metro.test.ts +++ b/test/integration/installed-package-metro.test.ts @@ -383,6 +383,7 @@ test('installed package exposes Node APIs and packaged companion tunnel entrypoi 'createAgentDeviceClient', 'createLocalArtifactAdapter', 'isAgentDeviceError', + 'isUnreadableCaptureContentError', 'normalizeAgentDeviceError', ]); assert.equal(imports.rootClientSnapshot, 'function'); diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index f6cc402fe0..306e87f20b 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -33,6 +33,9 @@ Entry points for driving devices from Node.js: - `createAgentDeviceClient(options?)` - `createLocalArtifactAdapter(options?)` - `AppError`, `isAgentDeviceError(error)`, `normalizeAgentDeviceError(error)` + - `isUnreadableCaptureContentError(error)`: true when a capture failed because the screen had no + readable app content yet (an Android helper content verdict, for example while the app is still + loading). Polling callers may keep polling; mechanism failures stay false. - `centerOfRect(rect)` - types: `AgentDeviceClient`, `AgentDeviceClientConfig`, `AgentDeviceDevice` - types: every option and result type of the client's methods, among them `AppOpenOptions`, From c6e028f15304f41b6f74c70b370da761985f62e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Thu, 15 Oct 2026 16:56:22 +0200 Subject: [PATCH 2/2] docs: scope isUnreadableCaptureContentError to android helper verdicts --- website/docs/docs/client-api.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/website/docs/docs/client-api.md b/website/docs/docs/client-api.md index 306e87f20b..54dec4c1b7 100644 --- a/website/docs/docs/client-api.md +++ b/website/docs/docs/client-api.md @@ -33,9 +33,10 @@ Entry points for driving devices from Node.js: - `createAgentDeviceClient(options?)` - `createLocalArtifactAdapter(options?)` - `AppError`, `isAgentDeviceError(error)`, `normalizeAgentDeviceError(error)` - - `isUnreadableCaptureContentError(error)`: true when a capture failed because the screen had no - readable app content yet (an Android helper content verdict, for example while the app is still - loading). Polling callers may keep polling; mechanism failures stay false. + - `isUnreadableCaptureContentError(error)`: true when an Android helper capture found no readable + app content yet (`empty-helper-output`, `system-window-only`, `content-poor-app-window`), for + example while the app is still loading. Polling callers may keep polling. False for every other + platform, backend, and mechanism failure. - `centerOfRect(rect)` - types: `AgentDeviceClient`, `AgentDeviceClientConfig`, `AgentDeviceDevice` - types: every option and result type of the client's methods, among them `AppOpenOptions`,