Skip to content

Commit 4778a27

Browse files
author
agent
committed
feat(daemon): land the selector capture seam with get as its first consumer
Takes ownership of the request-bound selector capture seam from #1876, which cannot ship standalone: with find's cutover deferred it had no consuming command (ADR 0019 §10) and was not dead-code clean (check:production-exports 19 -> 20). `get` is its first consumer, so it lands here. Adopts find's handoff as given. The one shape change, approved by the coordinator: the selector family gets its own capture uses carrying a PREFERRED `readTextAtPoint`, declared ALONGSIDE the snapshot uses so `snapshot`/`diff` keep binding exactly what they bind today. The read is surfaced through the existing arms of `bindSnapshotCaptureRuntime`, reusing the same selectActiveAppSnapshot / selectSnapshotWithoutActiveApp selectors — no second plan-to-operation dispatch. `get` now runs through `createBoundSelectorRuntime`; `resolveBoundGetRuntime` and its test are deleted as superseded, and `'get'` leaves the `createSelectorRuntime` capability union. The legacy read adapter survives for `find <q> get text` and is selected by which command constructed the runtime — never by failure, family, environment, or flag — so `get` cannot reach it. It retires in find's cutover, where the last consumer moves.
1 parent 9056a4c commit 4778a27

17 files changed

Lines changed: 659 additions & 464 deletions

packages/contracts/src/facades/platform.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,16 @@ export {
213213
appsRuntimeUse,
214214
captureSnapshotUse,
215215
defineUse,
216+
resolveSelectorCaptureRuntimePlan,
216217
resolveSnapshotRuntimePlan,
218+
selectorCaptureRuntimePlanUses,
217219
snapshotRuntimePlanUses,
218220
viewportRuntimeUse,
219221
} from '../platform-runtime-operations.ts';
220-
export type { SnapshotRuntimePlan } from '../platform-runtime-operations.ts';
222+
export type {
223+
SelectorCaptureRuntimePlan,
224+
SnapshotRuntimePlan,
225+
} from '../platform-runtime-operations.ts';
221226
export type {
222227
PlatformRuntimeHost,
223228
PlatformRuntimeModule,
@@ -233,13 +238,8 @@ export {
233238
appStateRuntimeUses,
234239
appStateUse,
235240
shutdownTargetUse,
236-
elementReadRuntimeUse,
237-
elementReadRuntimePlan,
238-
} from '../platform-runtime-operations.ts';
239-
export type {
240-
DeviceReadinessRuntimePlan,
241-
ElementReadRuntimePlan,
242241
} from '../platform-runtime-operations.ts';
242+
export type { DeviceReadinessRuntimePlan } from '../platform-runtime-operations.ts';
243243
export {
244244
bindLocalSnapshotInteractor,
245245
bindProviderSnapshotInteractor,

packages/contracts/src/platform-runtime-operations.ts

Lines changed: 75 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -69,30 +69,6 @@ export const bootTargetHeadlessUse = defineUse({
6969
export const appsRuntimeUse = defineUse({ required: ['ensureReady', 'listApps'] });
7070
export const captureSnapshotUse = defineUse({ required: ['captureSnapshot'] });
7171
export const viewportRuntimeUse = defineUse({ required: ['setViewport'] });
72-
/**
73-
* `get` reads one element's text or attributes. The required path answers from the captured
74-
* tree on every supported cell; `readTextAtPoint` is the owner-provided live read that recovers
75-
* fuller text for editable/expandable elements, so it is preferred rather than required (ADR
76-
* 0019 §2). An owner without it still executes `get` completely.
77-
*/
78-
export const elementReadRuntimeUse = defineUse({
79-
required: ['captureSnapshot'],
80-
preferred: ['readTextAtPoint'],
81-
});
82-
83-
/**
84-
* `get`'s use does not vary with its input, so there is nothing to resolve: the one plan is a
85-
* frozen constant rather than a `resolve…RuntimePlan` over a single row.
86-
*/
87-
export type ElementReadRuntimePlan = Readonly<{
88-
kind: 'element-read';
89-
use: typeof elementReadRuntimeUse;
90-
}>;
91-
92-
export const elementReadRuntimePlan: ElementReadRuntimePlan = Object.freeze({
93-
kind: 'element-read',
94-
use: elementReadRuntimeUse,
95-
});
9672
const captureSnapshotWithCustomActionsUse = defineUse({
9773
required: ['captureSnapshot', 'captureSnapshotWithCustomActions'],
9874
});
@@ -107,6 +83,31 @@ const captureSnapshotWithCustomActionsWithoutActiveAppUse = defineUse({
10783
],
10884
});
10985

86+
/**
87+
* The selector family's capture uses. Declared ALONGSIDE the snapshot uses above, never in place
88+
* of them: `snapshot`/`diff` keep binding exactly what they bind today. The only difference is the
89+
* PREFERRED element read — every selector read's required path answers from the captured tree, so
90+
* an owner without the read still executes the command completely (ADR 0019 §2), but an owner that
91+
* has one lets `get text` return the live value a truncated snapshot node cannot.
92+
*/
93+
const selectorCaptureUse = defineUse({
94+
required: ['captureSnapshot'],
95+
preferred: ['readTextAtPoint'],
96+
});
97+
const selectorCaptureWithoutActiveAppUse = defineUse({
98+
required: ['captureSnapshot', 'captureSnapshotWithoutActiveApp'],
99+
preferred: ['readTextAtPoint'],
100+
});
101+
102+
/**
103+
* The selector family (`find`, `get`, `is`, `wait`) resolves targets from the plain accessibility
104+
* capture: it exposes no `--actions` surface, so only the active-app split applies.
105+
*/
106+
export const selectorCaptureRuntimePlanUses = Object.freeze([
107+
selectorCaptureUse,
108+
selectorCaptureWithoutActiveAppUse,
109+
] as const);
110+
110111
export const snapshotRuntimePlanUses = Object.freeze([
111112
captureSnapshotUse,
112113
captureSnapshotWithCustomActionsUse,
@@ -136,30 +137,67 @@ export type SnapshotRuntimePlan =
136137
use: typeof captureSnapshotWithoutActiveAppUse;
137138
}>;
138139

140+
/**
141+
* Same two `kind`s the snapshot plan uses for this split — deliberately, so the shared
142+
* admit-then-bind path keeps ONE set of arms rather than growing a parallel dispatch — but
143+
* carrying the selector uses, which add the preferred element read.
144+
*/
145+
export type SelectorCaptureRuntimePlan =
146+
| Readonly<{
147+
kind: 'selector-active-app';
148+
operation: 'captureSnapshot';
149+
use: typeof selectorCaptureUse;
150+
}>
151+
| Readonly<{
152+
kind: 'selector-without-active-app';
153+
operation: 'captureSnapshotWithoutActiveApp';
154+
use: typeof selectorCaptureWithoutActiveAppUse;
155+
}>;
156+
157+
/**
158+
* The active-app split every selector capture selects from. The selector family exposes no
159+
* `--actions` surface, so custom actions are outside its declaration.
160+
*/
161+
export function resolveSelectorCaptureRuntimePlan(
162+
input: Readonly<{ hasActiveApp: boolean }>,
163+
): SelectorCaptureRuntimePlan {
164+
return input.hasActiveApp
165+
? Object.freeze({
166+
kind: 'selector-active-app',
167+
operation: 'captureSnapshot',
168+
use: selectorCaptureUse,
169+
})
170+
: Object.freeze({
171+
kind: 'selector-without-active-app',
172+
operation: 'captureSnapshotWithoutActiveApp',
173+
use: selectorCaptureWithoutActiveAppUse,
174+
});
175+
}
176+
139177
/** Selects one owner-fact-backed capture plan from normalized command/session intent. */
140178
export function resolveSnapshotRuntimePlan(input: {
141179
customActions: boolean;
142180
hasActiveApp: boolean;
143181
}): SnapshotRuntimePlan {
144-
if (input.customActions) {
182+
if (!input.customActions) {
145183
return input.hasActiveApp
146-
? Object.freeze({
147-
kind: 'custom-actions-active-app',
148-
operation: 'captureSnapshotWithCustomActions',
149-
use: captureSnapshotWithCustomActionsUse,
150-
})
184+
? Object.freeze({ kind: 'active-app', operation: 'captureSnapshot', use: captureSnapshotUse })
151185
: Object.freeze({
152-
kind: 'custom-actions-without-active-app',
153-
operation: 'captureSnapshotWithCustomActions',
154-
use: captureSnapshotWithCustomActionsWithoutActiveAppUse,
186+
kind: 'without-active-app',
187+
operation: 'captureSnapshotWithoutActiveApp',
188+
use: captureSnapshotWithoutActiveAppUse,
155189
});
156190
}
157191
return input.hasActiveApp
158-
? Object.freeze({ kind: 'active-app', operation: 'captureSnapshot', use: captureSnapshotUse })
192+
? Object.freeze({
193+
kind: 'custom-actions-active-app',
194+
operation: 'captureSnapshotWithCustomActions',
195+
use: captureSnapshotWithCustomActionsUse,
196+
})
159197
: Object.freeze({
160-
kind: 'without-active-app',
161-
operation: 'captureSnapshotWithoutActiveApp',
162-
use: captureSnapshotWithoutActiveAppUse,
198+
kind: 'custom-actions-without-active-app',
199+
operation: 'captureSnapshotWithCustomActions',
200+
use: captureSnapshotWithCustomActionsWithoutActiveAppUse,
163201
});
164202
}
165203
export const deviceBootRuntimeUses = Object.freeze([bootTargetUse, bootTargetHeadlessUse] as const);

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -521,13 +521,18 @@ export const MIGRATED_COMMAND_CUTOVERS: readonly MigratedCommandCutover[] = [
521521
routeNames: ['WEB_QUERY_COMMANDS_WITH_GET', 'HARMONYOS_GET_SUPPORT'],
522522
},
523523
runtimeTypeNames: ['ElementTextRuntimeOperations', 'SnapshotRuntimeOperations'],
524-
operations: { names: ['captureSnapshot', 'readTextAtPoint'] },
524+
operations: {
525+
names: ['captureSnapshot', 'captureSnapshotWithoutActiveApp', 'readTextAtPoint'],
526+
},
525527
singularExecution: {
526528
routes: ['dispatchGetViaRuntime'],
527-
operations: ['captureSnapshot', 'readTextAtPoint'],
529+
operations: ['captureSnapshot', 'captureSnapshotWithoutActiveApp', 'readTextAtPoint'],
530+
// `get` executes through the shared selector seam, so the capture owners are the SAME
531+
// selectors `snapshot`/`diff` count; only the preferred element read is this unit's own.
528532
operationOwners: {
529-
captureSnapshot: ['selectElementReadOperations'],
530-
readTextAtPoint: ['selectElementTextRead'],
533+
captureSnapshot: ['selectActiveAppSnapshot'],
534+
captureSnapshotWithoutActiveApp: ['selectSnapshotWithoutActiveApp'],
535+
readTextAtPoint: ['bindElementRead'],
531536
},
532537
},
533538
},

src/__tests__/test-utils/session-factories.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export function makeIosSession(name: string, overrides?: Partial<SessionState>):
5151
return makeSession(name, { device: IOS_SIMULATOR, ...overrides });
5252
}
5353

54+
/**
55+
* An iOS session with a tracked app — what `open <app>` produces. The shared snapshot
56+
* runtime exposes capture on an iOS leaf only through the active-app plan row, so a test
57+
* that captures on iOS needs this rather than a bare session.
58+
*/
59+
export function makeIosAppSession(name: string, overrides?: Partial<SessionState>): SessionState {
60+
return makeIosSession(name, { appBundleId: 'com.example.app', ...overrides });
61+
}
62+
5463
export function makeAndroidSession(name: string, overrides?: Partial<SessionState>): SessionState {
5564
return makeSession(name, { device: ANDROID_EMULATOR, ...overrides });
5665
}

src/core/command-descriptor/registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
readySendPushNotificationUse,
2727
openApplicationRuntimePlanUses,
2828
closeApplicationRuntimePlanUses,
29-
elementReadRuntimeUse,
29+
selectorCaptureRuntimePlanUses,
3030
snapshotRuntimePlanUses,
3131
prepareAppleRunnerRuntimeUse,
3232
runtimeCommandRuntimePlanUses,
@@ -1190,7 +1190,7 @@ export const RAW_COMMAND_DESCRIPTORS = [
11901190
daemon: { route: 'interaction', refFrameEffect: 'preserve' },
11911191
timeoutPolicy: postActionObservationTimeoutPolicy('get', PRESERVE_DAEMON_TIMEOUT_POLICY),
11921192
batchable: true,
1193-
platformExecution: { kind: 'device-runtime', uses: [elementReadRuntimeUse] as const },
1193+
platformExecution: { kind: 'device-runtime', uses: selectorCaptureRuntimePlanUses },
11941194
},
11951195
{
11961196
name: 'read',

0 commit comments

Comments
 (0)