Skip to content

Commit 9258535

Browse files
committed
refactor: consolidate desktop snapshot capture
1 parent b893163 commit 9258535

9 files changed

Lines changed: 77 additions & 107 deletions

scripts/layering/cutover-policy-ast.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ export function propertyName(node: unknown): string | undefined {
1919
: undefined;
2020
}
2121

22+
export function memberPath(node: unknown): string[] | undefined {
23+
if (node === null || typeof node !== 'object') return undefined;
24+
const record = node as Record<string, unknown>;
25+
if (record.type === 'Identifier') {
26+
return typeof record.name === 'string' ? [record.name] : undefined;
27+
}
28+
if (record.type === 'ChainExpression') return memberPath(record.expression);
29+
if (record.type !== 'MemberExpression' || record.computed === true) return undefined;
30+
const object = memberPath(record.object);
31+
const name = propertyName(record.property);
32+
return object && name ? [...object, name] : undefined;
33+
}
34+
2235
export function visitAst(node: unknown, visitor: (node: Record<string, unknown>) => void): void {
2336
if (node === null || typeof node !== 'object') return;
2437
if (Array.isArray(node)) {

scripts/layering/platform-package-source-policy.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { parseSync } from 'oxc-parser';
2+
import { memberPath } from './cutover-policy-ast.ts';
23
import { parseImports, type LayeringViolation } from './model.ts';
34

45
const RULE = 'R13 platform-package-substrate';
@@ -38,21 +39,6 @@ function rootIdentifier(node: unknown): string | undefined {
3839
return undefined;
3940
}
4041

41-
function memberPath(node: unknown): string[] | undefined {
42-
if (node === null || typeof node !== 'object') return undefined;
43-
const record = node as Record<string, unknown>;
44-
if (record['type'] === 'Identifier') {
45-
const name = record['name'];
46-
return typeof name === 'string' ? [name] : undefined;
47-
}
48-
if (record['type'] === 'ChainExpression') return memberPath(record['expression']);
49-
if (record['type'] !== 'MemberExpression' || record['computed'] === true) return undefined;
50-
const object = memberPath(record['object']);
51-
const property = record['property'] as Record<string, unknown> | undefined;
52-
const name = property?.['type'] === 'Identifier' ? property['name'] : undefined;
53-
return object && typeof name === 'string' ? [...object, name] : undefined;
54-
}
55-
5642
function literalString(node: unknown): string | undefined {
5743
if (node === null || typeof node !== 'object') return undefined;
5844
const value = (node as Record<string, unknown>)['value'];

scripts/layering/runtime-command-cutover-snapshot.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { parseSync } from 'oxc-parser';
2-
import { propertyName, visitAst } from './cutover-policy-ast.ts';
2+
import { memberPath, visitAst } from './cutover-policy-ast.ts';
33
import { countNamedCalls, lineOf, namedFunction } from './runtime-command-cutover-ast.ts';
44
import type { UnruledViolation } from './runtime-command-cutover-model.ts';
55

@@ -78,20 +78,6 @@ export function snapshotPlatformPolicyBranchViolations(
7878
return violations;
7979
}
8080

81-
function memberPath(node: unknown): string[] | undefined {
82-
if (node === null || typeof node !== 'object') return undefined;
83-
const record = node as AstNode;
84-
if (record['type'] === 'Identifier') {
85-
const name = record['name'];
86-
return typeof name === 'string' ? [name] : undefined;
87-
}
88-
if (record['type'] === 'ChainExpression') return memberPath(record['expression']);
89-
if (record['type'] !== 'MemberExpression' || record['computed'] === true) return undefined;
90-
const object = memberPath(record['object']);
91-
const name = propertyName(record['property']);
92-
return object && name ? [...object, name] : undefined;
93-
}
94-
9581
function samePath(actual: readonly string[], expected: readonly string[]): boolean {
9682
return (
9783
actual.length === expected.length && actual.every((part, index) => part === expected[index])

src/core/interactors/linux.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import {
1919
} from '../../platforms/linux/input-actions.ts';
2020
import { singlePointerPlanEndpoints } from '@agent-device/contracts/interaction';
2121
import { screenshotLinux } from '../../platforms/linux/screenshot.ts';
22-
import { snapshotLinux } from '../../platforms/linux/snapshot.ts';
23-
import { shapeDesktopSurfaceSnapshot } from '../../snapshot/snapshot-desktop-surface.ts';
22+
import { captureLinuxSurfaceSnapshot } from '../../snapshot/snapshot-desktop-surface.ts';
2423
import type { Interactor } from '@agent-device/contracts/interaction';
2524

2625
export function createLinuxInteractor(): Interactor {
@@ -47,19 +46,11 @@ export function createLinuxInteractor(): Interactor {
4746
},
4847
screenshot: (outPath, options) => screenshotLinux(outPath, options),
4948
snapshot: async (options) => {
50-
const result = await withDiagnosticTimer(
49+
return await withDiagnosticTimer(
5150
'snapshot_capture',
52-
async () => await snapshotLinux(options?.surface, options?.signal),
51+
async () => await captureLinuxSurfaceSnapshot(options, options?.signal),
5352
{ backend: 'linux-atspi' },
5453
);
55-
return shapeDesktopSurfaceSnapshot(
56-
{
57-
nodes: result.nodes ?? [],
58-
truncated: result.truncated ?? false,
59-
backend: 'linux-atspi',
60-
},
61-
options ?? {},
62-
);
6354
},
6455
back: () => backLinux(),
6556
home: () => homeLinux(),

src/platform-runtime-operation-host.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { createAppleApplicationTools } from './platform-runtime-apple-applicatio
2727
import { createAndroidApplicationTools } from './platform-runtime-android-application-tools.ts';
2828
import { createLocalApplicationInteractorHost } from './platform-runtime-local-application-interactors.ts';
2929
import { createApplicationResourceLifecycle } from './platform-runtime-application-resources.ts';
30-
import { createSnapshotRuntimeHost } from './platform-runtime-snapshot-host.ts';
30+
import { createSnapshotRuntimeHost } from './snapshot/snapshot-desktop-surface.ts';
3131

3232
export function createPlatformRuntimeHost(options: {
3333
sessionsDir: string;

src/platform-runtime-snapshot-host.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/platforms/apple/interactor.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { captureScreenshotViaRunner } from './core/screenshot.ts';
1111
import { iosRunnerOverrides, resolveAppleBackRunnerCommand } from './interactions.ts';
1212
import { appleRemotePressCommand } from './os/tvos/remote.ts';
13-
import { runMacOsScreenshotAction, runMacOsSnapshotAction } from './os/macos/helper.ts';
13+
import { runMacOsScreenshotAction } from './os/macos/helper.ts';
1414
import { runAppleRunnerCommand } from './core/runner/runner-client.ts';
1515
import {
1616
withAppleRunnerProvider,
@@ -32,7 +32,7 @@ import type {
3232
SnapshotOptions,
3333
} from '@agent-device/contracts/interaction';
3434
import { readSnapshotQualityVerdict } from '../../snapshot-quality/verdict.ts';
35-
import { shapeDesktopSurfaceSnapshot } from '../../snapshot/snapshot-desktop-surface.ts';
35+
import { captureMacOsSurfaceSnapshot } from '../../snapshot/snapshot-desktop-surface.ts';
3636

3737
export function createAppleInteractor(
3838
device: DeviceInfo,
@@ -151,24 +151,11 @@ async function captureAppleSnapshot(
151151
runnerOpts: RunnerCallOptions,
152152
) {
153153
if (isMacOs(device) && options?.surface && options.surface !== 'app') {
154-
return await captureMacOsSurfaceSnapshot(options.surface, options);
154+
return await captureMacOsSurfaceSnapshot(options, options.signal);
155155
}
156156
return await captureAppleRunnerSnapshot(device, options, runnerOpts);
157157
}
158158

159-
async function captureMacOsSurfaceSnapshot(
160-
surface: Exclude<NonNullable<SnapshotOptions['surface']>, 'app'>,
161-
options: SnapshotOptions,
162-
) {
163-
return shapeDesktopSurfaceSnapshot(
164-
await runMacOsSnapshotAction(surface, {
165-
bundleId: surface === 'menubar' ? options.appBundleId : undefined,
166-
signal: options.signal,
167-
}),
168-
options,
169-
);
170-
}
171-
172159
async function captureAppleRunnerSnapshot(
173160
device: DeviceInfo,
174161
options: SnapshotOptions | undefined,

src/platform-runtime-snapshot-host.test.ts renamed to src/snapshot/snapshot-desktop-surface.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ const { runMacOsSnapshotAction, snapshotLinux } = vi.hoisted(() => ({
66
snapshotLinux: vi.fn(),
77
}));
88

9-
vi.mock('./platforms/apple/os/macos/helper.ts', () => ({ runMacOsSnapshotAction }));
10-
vi.mock('./platforms/linux/snapshot.ts', () => ({ snapshotLinux }));
9+
vi.mock('../platforms/apple/os/macos/helper.ts', () => ({ runMacOsSnapshotAction }));
10+
vi.mock('../platforms/linux/snapshot.ts', () => ({ snapshotLinux }));
1111

12-
import { createSnapshotRuntimeHost } from './platform-runtime-snapshot-host.ts';
12+
import { createSnapshotRuntimeHost } from './snapshot-desktop-surface.ts';
1313

1414
const macosDevice = {
1515
id: 'desktop',
@@ -35,6 +35,7 @@ test('Apple snapshot host preserves non-app macOS surface capture and menubar id
3535
runMacOsSnapshotAction.mockResolvedValue({
3636
nodes: [{ index: 0, depth: 0, type: 'MenuBar', label: 'System menu' }],
3737
truncated: false,
38+
backend: 'macos-helper',
3839
});
3940
const signal = new AbortController().signal;
4041

src/snapshot/snapshot-desktop-surface.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,57 @@
11
import { attachRefs, type RawSnapshotNode } from '@agent-device/kernel/snapshot';
22
import type { SnapshotOptions, SnapshotResult } from '@agent-device/contracts/interaction';
3+
import type { CaptureSnapshotInput, SnapshotRuntimeHost } from '@agent-device/contracts/platform';
4+
import type { DeviceInfo } from '@agent-device/kernel/device';
35
import { findNodeByLabel } from './snapshot-node-label.ts';
46

7+
type SnapshotSurfaceOptions = NonNullable<CaptureSnapshotInput['options']>;
8+
9+
export function createSnapshotRuntimeHost(): SnapshotRuntimeHost {
10+
return Object.freeze({ captureSurface });
11+
}
12+
13+
export async function captureLinuxSurfaceSnapshot(
14+
options: CaptureSnapshotInput['options'],
15+
signal?: AbortSignal,
16+
) {
17+
const { snapshotLinux } = await import('../platforms/linux/snapshot.ts');
18+
const result = await snapshotLinux(options?.surface, signal);
19+
return shapeDesktopSurfaceSnapshot(
20+
{ nodes: result.nodes, truncated: result.truncated, backend: 'linux-atspi' },
21+
options ?? {},
22+
);
23+
}
24+
25+
export async function captureMacOsSurfaceSnapshot(
26+
options: SnapshotSurfaceOptions,
27+
signal?: AbortSignal,
28+
) {
29+
const surface = options.surface;
30+
if (!surface || surface === 'app') {
31+
throw new TypeError('Apple surface capture requires a non-app macOS surface');
32+
}
33+
const { runMacOsSnapshotAction } = await import('../platforms/apple/os/macos/helper.ts');
34+
const result = await runMacOsSnapshotAction(surface, {
35+
bundleId: surface === 'menubar' ? options.appBundleId : undefined,
36+
signal,
37+
});
38+
return shapeDesktopSurfaceSnapshot(result, options);
39+
}
40+
41+
const captureSurface: SnapshotRuntimeHost['captureSurface'] = async (device, options, signal) => {
42+
if (device.platform === 'linux') {
43+
return await captureLinuxSurfaceSnapshot(options, signal);
44+
}
45+
requireMacOsSurfaceDevice(device);
46+
return await captureMacOsSurfaceSnapshot(options ?? {}, signal);
47+
};
48+
49+
function requireMacOsSurfaceDevice(device: DeviceInfo): void {
50+
if (device.platform !== 'apple' || device.appleOs !== 'macos') {
51+
throw new TypeError('Apple surface capture requires a non-app macOS surface');
52+
}
53+
}
54+
555
const INTERACTIVE_ROLE_TOKENS = [
656
'button',
757
'menu',
@@ -13,7 +63,7 @@ const INTERACTIVE_ROLE_TOKENS = [
1363
] as const;
1464

1565
/** Applies the legacy desktop-surface projection once for both runtime hosts and legacy capture. */
16-
export function shapeDesktopSurfaceSnapshot(
66+
function shapeDesktopSurfaceSnapshot(
1767
data: SnapshotResult,
1868
options: Pick<SnapshotOptions, 'depth' | 'interactiveOnly' | 'scope'>,
1969
): SnapshotResult {

0 commit comments

Comments
 (0)