Skip to content

Commit cf61ffb

Browse files
thymikeeclaude
andcommitted
refactor(daemon): split press admission and press target tests by what each asserts
interaction-touch-press-admission.test.ts: (a) 12 tests move to the daemon package on the touch surface fake; the #1654 provenance-mismatch test is (c) and moves to src/commands/interaction/runtime/interactions-preresolved-target.test.ts. interaction-touch-press.test.ts: (a) secondary-button projection, the find pre-resolved wiring (adopt, control, no lookup) and read-after-press capture stay in the daemon package; the macOS menubar wrapper promotion, hittable-ancestor promotion, full-screen non-promotion (interactions- target-promotion.test.ts) and the pre-resolved adopt/control/no-lookup/shared-guard cases (interactions-preresolved-target.test.ts) are (c) and run against the real command runtime. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 5b8b4d3 commit cf61ffb

5 files changed

Lines changed: 560 additions & 543 deletions

File tree

src/__tests__/daemon-composition/interaction/interaction-touch-press-admission.test.ts renamed to packages/daemon/src/execution/interaction/internal/interaction-touch-press-admission.test.ts

Lines changed: 22 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,35 @@
1-
import { test, expect, vi, beforeEach } from 'vitest';
1+
import { test, expect, beforeEach } from 'vitest';
22
import { attachRefs } from '@agent-device/kernel/snapshot';
3-
import { makeSessionStore } from '@agent-device/daemon/testing/__tests__/support/store-factory';
4-
import { activateCompleteRefFrame, refFrameState } from '@agent-device/daemon/testing/ref-frame';
3+
import { makeSessionStore } from '../../../__tests__/support/store-factory.ts';
4+
import { activateCompleteRefFrame, refFrameState } from '../../../core/ref-frame.ts';
5+
import { setSessionSnapshot, STALE_SNAPSHOT_REFS_WARNING } from '../../session-snapshot.ts';
56
import {
6-
setSessionSnapshot,
7-
STALE_SNAPSHOT_REFS_WARNING,
8-
} from '@agent-device/daemon/testing/session-snapshot';
9-
import { handleInteractionCommands } from '@agent-device/daemon/testing/interaction/index';
7+
installInteractorSnapshotResolution,
8+
mockInteractorSnapshot,
9+
} from '../../../__tests__/support/interactor-resolution-fixture.ts';
10+
import { touchCreateCommandSurface } from '../../../__tests__/support/touch-command-surface-fake.ts';
11+
import { runInteraction } from '../../../__tests__/support/touch-interaction-runner.ts';
1012
import {
1113
getRuntimeBindings,
1214
mockTapPoint,
1315
resetGetRuntimeFixture,
14-
} from '@agent-device/daemon/testing/__tests__/interaction-get-runtime-fixture';
16+
} from '../../__tests__/interaction-get-runtime-fixture.ts';
1517
import {
1618
contextFromFlags,
17-
findResolvedTarget,
18-
makeFindPreresolvedTree,
1919
makeSession,
2020
makeStaleRefSession,
2121
makeTwoButtonNodes,
22-
readPressPoint,
23-
} from '@agent-device/daemon/testing/interaction/internal/__tests__/interaction-touch-fixtures';
24-
import { runFindInternalClick, runInteraction } from './interaction-touch-runner.ts';
25-
import { testCreateCommandSurface } from '../command-surface-fixture.ts';
22+
} from './__tests__/interaction-touch-fixtures.ts';
23+
import { handleInteractionCommands } from '../index.ts';
2624

2725
// Whether a targeted touch may act, and on what: click-option and positional
28-
// validation, and the ADR 0014 @ref rules (staleness, pins, frame expiry,
29-
// locator provenance) that admit or reject before any device op.
30-
31-
const { mockRunAppleRunnerCommand } = vi.hoisted(() => ({
32-
mockRunAppleRunnerCommand: vi.fn(),
33-
}));
34-
35-
vi.mock('@agent-device/platform-android/mechanics', async (importOriginal) => {
36-
const actual = await importOriginal<typeof import('@agent-device/platform-android/mechanics')>();
37-
return {
38-
...actual,
39-
getAndroidScreenSize: vi.fn(async () => ({ width: 1344, height: 2992 })),
40-
getAndroidAppState: vi.fn(async () => ({})),
41-
getAndroidBlockingDialogObservation: vi.fn(async () => ({ status: 'clear' }) as const),
42-
};
43-
});
44-
45-
vi.mock('@agent-device/daemon/testing/snapshot-interactor-capture', () => ({
46-
captureSnapshotWithInteractor: vi.fn(),
47-
}));
48-
49-
vi.mock('@agent-device/platform-apple/runner/operations', async (importOriginal) => {
50-
const actual =
51-
await importOriginal<typeof import('@agent-device/platform-apple/runner/operations')>();
52-
return { ...actual, runAppleRunnerCommand: mockRunAppleRunnerCommand };
53-
});
54-
55-
import {
56-
getAndroidAppState,
57-
getAndroidBlockingDialogObservation,
58-
getAndroidScreenSize,
59-
} from '@agent-device/platform-android/mechanics';
60-
import { captureSnapshotWithInteractor } from '@agent-device/daemon/testing/snapshot-interactor-capture';
61-
const mockGetAndroidAppState = vi.mocked(getAndroidAppState);
62-
const mockGetAndroidBlockingDialogObservation = vi.mocked(getAndroidBlockingDialogObservation);
63-
const mockGetAndroidScreenSize = vi.mocked(getAndroidScreenSize);
64-
const mockCaptureSnapshotForSession = vi.mocked(captureSnapshotWithInteractor);
26+
// validation, and the ADR 0014 @ref rules (staleness, pins, frame expiry) that
27+
// admit or reject before any device op.
6528

6629
beforeEach(() => {
6730
resetGetRuntimeFixture();
68-
mockGetAndroidAppState.mockReset();
69-
mockGetAndroidAppState.mockResolvedValue({});
70-
mockGetAndroidBlockingDialogObservation.mockReset();
71-
mockGetAndroidBlockingDialogObservation.mockResolvedValue({ status: 'clear' });
72-
mockGetAndroidScreenSize.mockReset();
73-
mockGetAndroidScreenSize.mockResolvedValue({ width: 1344, height: 2992 });
74-
mockCaptureSnapshotForSession.mockReset();
75-
mockRunAppleRunnerCommand.mockReset();
76-
mockRunAppleRunnerCommand.mockResolvedValue({});
31+
installInteractorSnapshotResolution();
32+
mockInteractorSnapshot.mockReset();
7733
});
7834

7935
test('click --button middle on macOS fails with an explicit unsupported-operation error', async () => {
@@ -95,7 +51,7 @@ test('click --button middle on macOS fails with an explicit unsupported-operatio
9551
);
9652

9753
const response = await handleInteractionCommands({
98-
createCommandSurface: testCreateCommandSurface,
54+
createCommandSurface: touchCreateCommandSurface,
9955
req: {
10056
token: 't',
10157
session: sessionName,
@@ -123,7 +79,7 @@ test('press coordinates does not treat extra trailing args as selector', async (
12379
sessionStore.publish(sessionName, makeSession(sessionName));
12480

12581
const response = await handleInteractionCommands({
126-
createCommandSurface: testCreateCommandSurface,
82+
createCommandSurface: touchCreateCommandSurface,
12783
req: {
12884
token: 't',
12985
session: sessionName,
@@ -143,30 +99,12 @@ test('press coordinates does not treat extra trailing args as selector', async (
14399
expect(sessionStore.get(sessionName)?.actions.length).toBe(1);
144100
});
145101

146-
test('#1654: the resolved-target payload fails closed when its ref provenance disagrees', async () => {
147-
const sessionStore = makeSessionStore();
148-
const sessionName = 'find-preresolved-mismatched-ref';
149-
sessionStore.publish(sessionName, makeStaleRefSession(sessionName));
150-
const preresolved = makeFindPreresolvedTree();
151-
152-
const response = await runFindInternalClick(sessionStore, sessionName, {
153-
findResolvedTarget: findResolvedTarget(preresolved, '@e9'),
154-
});
155-
156-
expect(response?.ok).toBe(false);
157-
if (response && !response.ok) {
158-
expect(response.error.code).toBe('COMMAND_FAILED');
159-
expect(response.error.message).toContain('provenance does not match');
160-
}
161-
expect(readPressPoint(mockTapPoint)).toBeUndefined();
162-
});
163-
164102
test('press selector then press @ref rejects refs that outlived the stored snapshot before dispatch', async () => {
165103
const sessionStore = makeSessionStore();
166104
const sessionName = 'stale-ref-warns';
167105
const session = makeStaleRefSession(sessionName);
168106
sessionStore.publish(sessionName, session);
169-
mockCaptureSnapshotForSession.mockResolvedValue({
107+
mockInteractorSnapshot.mockResolvedValue({
170108
nodes: makeTwoButtonNodes(),
171109
backend: 'xctest',
172110
producer: 'apple-runner',
@@ -202,7 +140,7 @@ test('a ref press crosses the ADR 0014 side-effect seam and expires the ref fram
202140
const sessionName = 'seam-expiry';
203141
const session = makeStaleRefSession(sessionName);
204142
sessionStore.publish(sessionName, session);
205-
mockCaptureSnapshotForSession.mockResolvedValue({
143+
mockInteractorSnapshot.mockResolvedValue({
206144
nodes: makeTwoButtonNodes(),
207145
backend: 'xctest',
208146
producer: 'apple-runner',
@@ -225,7 +163,7 @@ test('ADR 0014 evidence #1: a second ref mutation rejects (bare and pinned) unti
225163
// A complete snapshot issued the frame at generation 500.
226164
activateCompleteRefFrame(session);
227165
sessionStore.publish(sessionName, session);
228-
mockCaptureSnapshotForSession.mockResolvedValue({
166+
mockInteractorSnapshot.mockResolvedValue({
229167
nodes: makeTwoButtonNodes(),
230168
backend: 'xctest',
231169
producer: 'apple-runner',
@@ -270,7 +208,7 @@ test('re-issuing a complete frame lets press @ref succeed again without warning'
270208
const sessionName = 'reissued-refs-no-warning';
271209
const session = makeStaleRefSession(sessionName);
272210
sessionStore.publish(sessionName, session);
273-
mockCaptureSnapshotForSession.mockResolvedValue({
211+
mockInteractorSnapshot.mockResolvedValue({
274212
nodes: makeTwoButtonNodes(),
275213
backend: 'xctest',
276214
producer: 'apple-runner',
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import { test, expect, beforeEach } from 'vitest';
2+
import { attachRefs } from '@agent-device/kernel/snapshot';
3+
import { makeIosSession } from '../../../__tests__/support/session-factories.ts';
4+
import { makeSessionStore } from '../../../__tests__/support/store-factory.ts';
5+
import {
6+
installInteractorSnapshotResolution,
7+
mockInteractorSnapshot,
8+
} from '../../../__tests__/support/interactor-resolution-fixture.ts';
9+
import { touchCreateCommandSurface } from '../../../__tests__/support/touch-command-surface-fake.ts';
10+
import { runFindInternalClick } from '../../../__tests__/support/touch-interaction-runner.ts';
11+
import {
12+
getRuntimeBindings,
13+
mockTapPoint,
14+
resetGetRuntimeFixture,
15+
} from '../../__tests__/interaction-get-runtime-fixture.ts';
16+
import {
17+
contextFromFlags,
18+
findResolvedTarget,
19+
makeFindPreresolvedTree,
20+
makeSession,
21+
makeStaleRefSession,
22+
readPressPoint,
23+
} from './__tests__/interaction-touch-fixtures.ts';
24+
import { handleInteractionCommands } from '../index.ts';
25+
26+
// How an admitted press/click reaches the device: the button projection onto the bound tap, a
27+
// mutating find's pre-resolved node (#1654) handed to the command surface, and the capture a read
28+
// takes after a press.
29+
30+
beforeEach(() => {
31+
resetGetRuntimeFixture();
32+
installInteractorSnapshotResolution();
33+
mockInteractorSnapshot.mockReset();
34+
});
35+
36+
test('click --button secondary on @ref dispatches a secondary press on macOS and records click', async () => {
37+
const sessionStore = makeSessionStore();
38+
const sessionName = 'default';
39+
const session = makeSession(sessionName);
40+
session.device = {
41+
platform: 'apple',
42+
appleOs: 'macos',
43+
id: 'macos-desktop',
44+
name: 'My Mac',
45+
kind: 'device',
46+
booted: true,
47+
};
48+
session.snapshot = {
49+
nodes: attachRefs([
50+
{
51+
index: 0,
52+
type: 'XCUIElementTypeCell',
53+
label: 'failed-step.json',
54+
rect: { x: 400, y: 500, width: 200, height: 20 },
55+
enabled: true,
56+
hittable: true,
57+
},
58+
]),
59+
createdAt: Date.now(),
60+
backend: 'xctest',
61+
};
62+
sessionStore.publish(sessionName, session);
63+
64+
mockTapPoint.mockResolvedValue({ button: 'secondary' });
65+
66+
const response = await handleInteractionCommands({
67+
createCommandSurface: touchCreateCommandSurface,
68+
req: {
69+
token: 't',
70+
session: sessionName,
71+
command: 'click',
72+
positionals: ['@e1'],
73+
flags: { clickButton: 'secondary' },
74+
},
75+
sessionName,
76+
sessionStore,
77+
contextFromFlags,
78+
...getRuntimeBindings(),
79+
});
80+
81+
expect(response).toBeTruthy();
82+
expect(response?.ok).toBe(true);
83+
expect(readPressPoint(mockTapPoint)).toEqual(['500', '510']);
84+
expect(mockTapPoint.mock.calls[0]?.[0].options.button).toBe('secondary');
85+
if (response?.ok) {
86+
expect(response.data?.button).toBe('secondary');
87+
expect(response.data?.ref).toBe('e1');
88+
}
89+
90+
const stored = sessionStore.get(sessionName);
91+
expect(stored).toBeTruthy();
92+
expect(stored?.actions[0]?.command).toBe('click');
93+
expect(stored?.actions[0]?.flags.clickButton).toBe('secondary');
94+
});
95+
96+
test('#1654: a mutating find click acts on the node find resolved, not a re-resolved @ref', async () => {
97+
const sessionStore = makeSessionStore();
98+
const sessionName = 'find-preresolved-click';
99+
sessionStore.publish(sessionName, makeStaleRefSession(sessionName));
100+
const preresolved = makeFindPreresolvedTree();
101+
102+
const response = await runFindInternalClick(sessionStore, sessionName, {
103+
findResolvedTarget: findResolvedTarget(preresolved),
104+
});
105+
106+
expect(response?.ok).toBe(true);
107+
// find's node — (300,500,20,20) → (310, 510). A second resolution would read
108+
// @e2 out of the SESSION frame tree instead and tap (60, 40).
109+
expect(readPressPoint(mockTapPoint)).toEqual(['310', '510']);
110+
});
111+
112+
test('#1654 control: without the resolved-target payload the same click still resolves @ref from the session tree', async () => {
113+
const sessionStore = makeSessionStore();
114+
const sessionName = 'find-preresolved-control';
115+
sessionStore.publish(sessionName, makeStaleRefSession(sessionName));
116+
117+
const response = await runFindInternalClick(sessionStore, sessionName, {});
118+
119+
// The ordinary ref path is untouched: @e1 is "Continue" at (10,20,100,40).
120+
expect(response?.ok).toBe(true);
121+
expect(readPressPoint(mockTapPoint)).toEqual(['60', '40']);
122+
});
123+
124+
test('#1654: the leaf performs no ref lookup at all — a session that could not resolve @e2 still acts', async () => {
125+
// Defensive proof of the structural contract: this synthetic session has no
126+
// stored snapshot, so any second resolution necessarily fails ("Ref @e2 not
127+
// found or has no bounds"). Production find normally stores its capture;
128+
// this test proves lookup absence without claiming that missing state occurs.
129+
const sessionStore = makeSessionStore();
130+
const sessionName = 'find-preresolved-no-session-tree';
131+
const session = makeSession(sessionName);
132+
session.snapshot = undefined;
133+
sessionStore.publish(sessionName, session);
134+
const preresolved = makeFindPreresolvedTree();
135+
136+
const withoutPreresolution = await runFindInternalClick(sessionStore, sessionName, {});
137+
expect(withoutPreresolution?.ok).toBe(false);
138+
139+
const response = await runFindInternalClick(sessionStore, sessionName, {
140+
findResolvedTarget: findResolvedTarget(preresolved),
141+
});
142+
143+
expect(response?.ok).toBe(true);
144+
expect(readPressPoint(mockTapPoint)).toEqual(['310', '510']);
145+
});
146+
147+
test('a read right after a press captures the post-tap screen instead of reusing the pre-tap tree', async () => {
148+
const sessionStore = makeSessionStore();
149+
const sessionName = 'read-after-press';
150+
sessionStore.publish(
151+
sessionName,
152+
makeIosSession(sessionName, { appBundleId: 'com.example.app' }),
153+
);
154+
const screen = (step: string) => ({
155+
backend: 'xctest' as const,
156+
producer: 'apple-runner' as const,
157+
nodes: [
158+
{ index: 0, depth: 0, type: 'Application', rect: { x: 0, y: 0, width: 393, height: 852 } },
159+
{
160+
index: 1,
161+
depth: 1,
162+
parentIndex: 0,
163+
type: 'Button',
164+
label: 'Next',
165+
rect: { x: 24, y: 120, width: 120, height: 44 },
166+
enabled: true,
167+
hittable: true,
168+
},
169+
{
170+
index: 2,
171+
depth: 1,
172+
parentIndex: 0,
173+
type: 'StaticText',
174+
label: step,
175+
identifier: 'step',
176+
rect: { x: 24, y: 220, width: 320, height: 24 },
177+
},
178+
],
179+
});
180+
mockInteractorSnapshot
181+
.mockResolvedValueOnce(screen('Step 1'))
182+
.mockResolvedValue(screen('Step 2'));
183+
const run = async (command: string, positionals: string[]) =>
184+
await handleInteractionCommands({
185+
createCommandSurface: touchCreateCommandSurface,
186+
req: { token: 't', session: sessionName, command, positionals, flags: {} },
187+
sessionName,
188+
sessionStore,
189+
contextFromFlags,
190+
...getRuntimeBindings(),
191+
});
192+
193+
expect(await run('is', ['visible', 'label=Next'])).toMatchObject({ ok: true });
194+
expect(await run('press', ['84', '142'])).toMatchObject({ ok: true });
195+
expect(mockInteractorSnapshot).toHaveBeenCalledTimes(1);
196+
197+
const read = await run('get', ['text', 'id="step"']);
198+
199+
expect(read).toMatchObject({ ok: true, data: { text: 'Step 2' } });
200+
expect(mockInteractorSnapshot).toHaveBeenCalledTimes(2);
201+
});

0 commit comments

Comments
 (0)