Skip to content

Commit 31fb491

Browse files
committed
refactor(snapshot): type the acquisition producer beside the platform channel
Three producers with different guarantees share the backend: 'xctest' stamp (Apple runner, Appium page-source, limrun element trees), and the 'android' channel conflates the local uiautomator path with Appium trees the same way. Add SnapshotProducer as a required field on SnapshotResult so the compiler enumerates every producer, and carry it into SnapshotState. Types-before-semantics step for #1983; no consumer behavior changes.
1 parent 759f175 commit 31fb491

34 files changed

Lines changed: 129 additions & 12 deletions

‎CONTEXT.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ A backend-owned accessibility value before snapshot presentation.
187187
**Snapshot acquisition**:
188188
One backend attempt's raw accessibility nodes and attempt-level capture facts.
189189

190+
**Snapshot producer**:
191+
The acquisition component that produced a snapshot's raw tree — the third identity axis beside
192+
the platform channel (`SnapshotBackend`) and the in-plan capture strategy
193+
(`SnapshotCaptureBackend`). One channel is fed by producers with different guarantees (the
194+
`xctest` channel carries Apple-runner, Appium page-source, and limrun element trees), so logic
195+
that assumes presentation, scope, or geometry guarantees keys on the producer, never on the
196+
channel alone.
197+
190198
**Presentation options**:
191199
The policy input controlling how one snapshot acquisition becomes a public projection.
192200

‎packages/contracts/src/interactor-types.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
Rect,
1515
SnapshotBackend,
1616
SnapshotOptions as BaseSnapshotOptions,
17+
SnapshotProducer,
1718
} from '@agent-device/kernel/snapshot';
1819

1920
export type RunnerContext = {
@@ -245,6 +246,11 @@ export type SnapshotResult = Omit<BackendSnapshotResult, 'backend' | 'nodes'> &
245246
SnapshotBackend,
246247
'android' | 'harmonyos-arkui' | 'xctest' | 'linux-atspi' | 'macos-helper' | 'web'
247248
>;
249+
/**
250+
* Required on every acquisition: the channel alone cannot say who acquired the tree or
251+
* which guarantees it carries (`SnapshotProducer` in `@agent-device/kernel/snapshot`).
252+
*/
253+
producer: SnapshotProducer;
248254
};
249255

250256
export type Interactor = {

‎packages/kernel/src/snapshot.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,25 @@ export type SnapshotBackend =
155155
| 'linux-atspi'
156156
| 'web';
157157

158+
/**
159+
* Which acquisition component produced a snapshot's raw tree — the third axis beside the
160+
* platform channel (`SnapshotBackend`) and the in-plan capture strategy
161+
* (`SnapshotCaptureBackend`). One channel is fed by several producers with different
162+
* guarantees: `xctest` trees come from the local Apple runner, Appium page-source XML, or a
163+
* limrun element tree, and only the runner's output has been through the runner's presentation
164+
* (clip fold, effective geometry, scope). Logic that assumes presentation, scope, or geometry
165+
* guarantees must key on the producer, never on the channel alone.
166+
*/
167+
export type SnapshotProducer =
168+
| 'apple-runner'
169+
| 'android-uiautomator'
170+
| 'appium-source'
171+
| 'limrun-ios-tree'
172+
| 'macos-helper'
173+
| 'linux-atspi'
174+
| 'agent-browser'
175+
| 'harmonyos-uitest';
176+
158177
export function isSnapshotBackend(value: unknown): value is SnapshotBackend {
159178
return (
160179
value === 'xctest' ||
@@ -180,6 +199,7 @@ export type SnapshotState = {
180199
createdAt: number;
181200
truncated?: boolean;
182201
backend?: SnapshotBackend;
202+
producer?: SnapshotProducer;
183203
snapshotQuality?: SnapshotQualityVerdict;
184204
comparisonSafe?: boolean;
185205
presentationKey?: string;

‎packages/platform-apple/src/runtime.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ test.each(['frontmost-app', 'desktop', 'menubar'] as const)(
249249
const host = platformRuntimeHostFixture();
250250
const captureSurface = vi.fn(async () => ({
251251
backend: 'macos-helper' as const,
252+
producer: 'macos-helper' as const,
252253
nodes: [],
253254
truncated: false,
254255
}));
@@ -551,6 +552,7 @@ test('the macOS surface branch composes the per-capture signal with the binding
551552
const host = platformRuntimeHostFixture();
552553
const captureSurface = vi.fn<SnapshotRuntimeHost['captureSurface']>(async () => ({
553554
backend: 'macos-helper' as const,
555+
producer: 'macos-helper' as const,
554556
nodes: [],
555557
truncated: false,
556558
}));

‎packages/platform-linux/src/runtime.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ test.each([
7979
async ({ device, legacy }) => {
8080
const captureSurface = vi.fn(async () => ({
8181
backend: 'linux-atspi' as const,
82+
producer: 'linux-atspi' as const,
8283
nodes: [],
8384
truncated: false,
8485
}));
@@ -144,6 +145,7 @@ test.each([
144145
function lifecycleHost(
145146
captureSurface: SnapshotRuntimeHost['captureSurface'] = async () => ({
146147
backend: 'linux-atspi' as const,
148+
producer: 'linux-atspi' as const,
147149
nodes: [],
148150
truncated: false,
149151
}),
@@ -209,6 +211,7 @@ function expectLinuxNavigationAndKeyboardFacts(
209211
test('the Linux surface capture composes the per-capture signal with the request scope signal', async () => {
210212
const captureSurface = vi.fn<SnapshotRuntimeHost['captureSurface']>(async () => ({
211213
backend: 'linux-atspi' as const,
214+
producer: 'linux-atspi' as const,
212215
nodes: [],
213216
truncated: false,
214217
}));

‎packages/provider-limrun/src/ios.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ class LimrunIosInteractor implements Interactor {
257257
async snapshot(_options?: SnapshotOptions): Promise<SnapshotResult> {
258258
const treeJson = await this.session.client.elementTree();
259259
const parsed = JSON.parse(treeJson) as IosTreeNode | IosTreeNode[];
260-
return { nodes: flattenIosTree(parsed), backend: 'xctest' };
260+
return { nodes: flattenIosTree(parsed), backend: 'xctest', producer: 'limrun-ios-tree' };
261261
}
262262

263263
async back(): Promise<void> {

‎packages/provider-webdriver/src/webdriver-interactor.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ class WebDriverInteractor implements Interactor {
282282
this.requireSupport('snapshot');
283283
return {
284284
backend: this.backend,
285+
producer: 'appium-source',
285286
nodes: parseWebDriverSource(await this.client.source()),
286287
};
287288
}

‎src/core/interactors/android.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export function createAndroidInteractor(
112112
nodes: result.nodes ?? [],
113113
truncated: result.truncated ?? false,
114114
backend: 'android',
115+
producer: 'android-uiautomator',
115116
...snapshotCaptureAnnotationsFrom(result),
116117
});
117118
},

‎src/core/interactors/harmonyos.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function createHarmonyInteractor(device: DeviceInfo, _runner?: RunnerCont
4545
async () => await snapshotHarmony(device, options),
4646
{ backend: 'harmonyos-arkui' },
4747
);
48-
return { ...result, backend: 'harmonyos-arkui' };
48+
return { ...result, backend: 'harmonyos-arkui', producer: 'harmonyos-uitest' };
4949
},
5050
back: () => backHarmony(device),
5151
home: () => homeHarmony(device),

‎src/core/interactors/web.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function createWebInteractor(provider: WebProvider = resolveWebProvider()
5757
nodes: result.nodes,
5858
truncated: result.truncated ?? false,
5959
backend: 'web',
60+
producer: 'agent-browser',
6061
};
6162
},
6263
setOrientation: async () => {

0 commit comments

Comments
 (0)