Skip to content

Commit ec6b5e6

Browse files
committed
refactor(snapshot): clean snapshot ownership
1 parent 057ab1c commit ec6b5e6

61 files changed

Lines changed: 706 additions & 692 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

fallow-baselines/health.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,6 @@
580580
"src/compat/maestro/support.ts:high impact",
581581
"src/daemon/session-routing.ts:high impact",
582582
"src/daemon/handlers/session-state.ts:complexity",
583-
"src/snapshot/snapshot-processing.ts:high impact",
584583
"src/commands/cli-grammar/common.ts:high impact",
585584
"src/daemon/snapshot-presentation/tree.ts:high impact",
586585
"src/utils/success-text.ts:high impact",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import {
4+
ANDROID_CONTENT_RECOVERY_REASONS,
5+
isAndroidContentRecoveryReason,
6+
isUnreadableCaptureContentError,
7+
} from './facades/platform.ts';
8+
9+
test('Android content-recovery reasons are a single guarded taxonomy', () => {
10+
assert.deepEqual(ANDROID_CONTENT_RECOVERY_REASONS, [
11+
'empty-helper-output',
12+
'system-window-only',
13+
'content-poor-app-window',
14+
]);
15+
for (const reason of ANDROID_CONTENT_RECOVERY_REASONS) {
16+
assert.equal(isAndroidContentRecoveryReason(reason), true);
17+
assert.equal(
18+
isUnreadableCaptureContentError({ details: { androidSnapshotHelperFailureReason: reason } }),
19+
true,
20+
);
21+
}
22+
assert.equal(isAndroidContentRecoveryReason('helper-timeout'), false);
23+
assert.equal(
24+
isUnreadableCaptureContentError({
25+
details: { androidSnapshotHelperFailureReason: 'helper-timeout' },
26+
}),
27+
false,
28+
);
29+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Android helper content verdicts. The helper captured a tree successfully, but the
3+
* current screen did not contain enough readable application content to use as a
4+
* snapshot. Keep the enumeration here so the producer, waits, and replay recovery
5+
* cannot grow different retry taxonomies.
6+
*/
7+
export const ANDROID_CONTENT_RECOVERY_REASONS = [
8+
'empty-helper-output',
9+
'system-window-only',
10+
'content-poor-app-window',
11+
] as const;
12+
13+
export type AndroidContentRecoveryReason = (typeof ANDROID_CONTENT_RECOVERY_REASONS)[number];
14+
15+
const ANDROID_CONTENT_RECOVERY_REASON_SET: ReadonlySet<string> = new Set(
16+
ANDROID_CONTENT_RECOVERY_REASONS,
17+
);
18+
19+
export function isAndroidContentRecoveryReason(
20+
value: unknown,
21+
): value is AndroidContentRecoveryReason {
22+
return typeof value === 'string' && ANDROID_CONTENT_RECOVERY_REASON_SET.has(value);
23+
}
24+
25+
/**
26+
* True when a thrown Android capture failure is a content verdict rather than a
27+
* mechanism failure. Wait and replay polling may ride out these states; helper
28+
* timeouts, adb failures, and missing artifacts remain fail-fast.
29+
*/
30+
export function isUnreadableCaptureContentError(error: unknown): boolean {
31+
if (!error || typeof error !== 'object') return false;
32+
const details = (error as { details?: unknown }).details;
33+
if (!details || typeof details !== 'object') return false;
34+
const reason = (details as Record<string, unknown>).androidSnapshotHelperFailureReason;
35+
return isAndroidContentRecoveryReason(reason);
36+
}

packages/contracts/src/facades/platform.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export type {
1212
AndroidInputOwnership,
1313
AndroidInputOwnershipSource,
1414
} from '../android-input-ownership.ts';
15+
export {
16+
ANDROID_CONTENT_RECOVERY_REASONS,
17+
isAndroidContentRecoveryReason,
18+
isUnreadableCaptureContentError,
19+
} from '../android-snapshot-quality.ts';
20+
export type { AndroidContentRecoveryReason } from '../android-snapshot-quality.ts';
1521
export {
1622
ANDROID_SYSTEM_CHROME_PACKAGE,
1723
hasAndroidSystemChromeProvenance,

packages/kernel/src/snapshot.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
* The daemon renders it; it never re-derives degradation from node shapes.
44
*
55
* Defined here (the foundational snapshot type module) rather than in
6-
* snapshot-quality.ts so SnapshotNode can reference it without a cyclic import;
7-
* snapshot-quality.ts (the validation logic) re-exports it for existing callers.
6+
* snapshot-quality/verdict.ts so SnapshotNode can reference it without a cyclic import;
7+
* snapshot-quality/verdict.ts owns the validation logic.
88
*/
99
/**
1010
* Which capture STRATEGY produced a snapshot, within one platform's plan —
@@ -175,7 +175,7 @@ export type SnapshotState = {
175175
/**
176176
* Android: the capture is an occluding system surface (notification shade, quick settings)
177177
* rather than app content. Consumers that surface this tree to the agent must disclose the
178-
* occlusion (see snapshot/system-surface-disclosure.ts).
178+
* occlusion (see core/android-system-surface-disclosure.ts).
179179
*/
180180
systemSurfaceOnly?: boolean;
181181
};

scripts/__tests__/help-conformance-sample-producers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import {
3333
import type { ConnectVerification } from '../../src/cli/connection/connect-provider-adapters.ts';
3434
import type { RemoteConnectionState } from '../../src/remote/remote-connection-state.ts';
3535
import { AppError, normalizeError } from '@agent-device/kernel/errors';
36-
import type { SnapshotQualityVerdict } from '../../src/snapshot/snapshot-quality.ts';
37-
import { renderSnapshotQualityWarnings } from '../../src/snapshot/snapshot-quality.ts';
36+
import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot';
37+
import { renderSnapshotQualityWarnings } from '../../src/snapshot-quality/warnings.ts';
3838
import { formatSnapshotText, printHumanError } from '../../src/utils/output.ts';
3939

4040
// The production renderer behind each captured sample in

scripts/help-conformance-sample-outputs.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ hint: The UI kept changing for the whole settle budget (animation, carousel, or
7272

7373
// Recovered snapshot: the private-ax fallback fired but still exposed
7474
// actionable refs. Warning wording is renderSnapshotQualityWarnings
75-
// (src/snapshot/snapshot-quality.ts); lines are the structured snapshot
75+
// (src/snapshot-quality/warnings.ts); lines are the structured snapshot
7676
// renderer (src/utils/output.ts formatSnapshotText).
7777
export const PRIVATE_AX_RECOVERY_SAMPLE = {
7878
command: 'agent-device snapshot -i',

scripts/layering/model.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ test('classifyZone separates the ranked spine from intentionally-unranked zones'
152152
// forbids daemon/ from importing commands/ so the files that wire them cannot be ranked.
153153
assert.equal(classifyZone('mcp'), 'ranked');
154154
assert.equal(classifyZone('snapshot'), 'ranked');
155+
assert.equal(classifyZone('snapshot-quality'), 'ranked');
155156
// A zone that is neither ranked nor listed peripheral must be flagged, never
156157
// silently treated as back-edge-free.
157158
assert.equal(classifyZone('not-a-real-zone'), 'unclassified');

scripts/layering/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const TARGET_DAG_RANK = new Map([
4343
['screenshot-diff', 1],
4444
['selectors', 1],
4545
['snapshot', 1],
46+
['snapshot-quality', 1],
4647
['utils', 1],
4748
['core', 2],
4849
['cli-schema', 3],

src/commands/capture/runtime/snapshot.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import {
1919
buildSnapshotDiff,
2020
countSnapshotComparableLines,
2121
} from '../../../snapshot/snapshot-diff.ts';
22-
import { renderSnapshotQualityWarnings } from '../../../snapshot/snapshot-quality.ts';
22+
import { renderSnapshotQualityWarnings } from '../../../snapshot-quality/warnings.ts';
2323
import { buildSnapshotVisibility } from '../../../snapshot/snapshot-visibility.ts';
24-
import { ANDROID_SYSTEM_SURFACE_DISCLOSURE } from '../../../snapshot/system-surface-disclosure.ts';
24+
import { ANDROID_SYSTEM_SURFACE_DISCLOSURE } from '../../../core/android-system-surface-disclosure.ts';
2525
import { formatReactNativeOverlayWarning } from '../../react-native/overlay.ts';
2626
import { now } from '../../runtime-common.ts';
2727
import type {

0 commit comments

Comments
 (0)