|
1 | 1 | import test from 'node:test'; |
2 | 2 | import assert from 'node:assert/strict'; |
3 | | -import { parseFindArgs } from '../find.ts'; |
| 3 | +import fs from 'node:fs'; |
| 4 | +import os from 'node:os'; |
| 5 | +import path from 'node:path'; |
| 6 | +import { parseFindArgs, handleFindCommands } from '../find.ts'; |
4 | 7 | import { AppError } from '../../../utils/errors.ts'; |
| 8 | +import { SessionStore } from '../../session-store.ts'; |
| 9 | +import type { SessionState } from '../../types.ts'; |
| 10 | +import type { DaemonRequest, DaemonResponse } from '../../types.ts'; |
| 11 | + |
| 12 | +function makeSessionStore(): SessionStore { |
| 13 | + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-find-handler-')); |
| 14 | + return new SessionStore(path.join(root, 'sessions')); |
| 15 | +} |
| 16 | + |
| 17 | +function makeSession(name: string): SessionState { |
| 18 | + return { |
| 19 | + name, |
| 20 | + device: { |
| 21 | + platform: 'ios', |
| 22 | + id: 'sim-1', |
| 23 | + name: 'iPhone 17 Pro', |
| 24 | + kind: 'simulator', |
| 25 | + booted: true, |
| 26 | + }, |
| 27 | + createdAt: Date.now(), |
| 28 | + actions: [], |
| 29 | + }; |
| 30 | +} |
| 31 | + |
| 32 | +const INCREMENT_NODE = { |
| 33 | + type: 'Button', |
| 34 | + label: 'Increment', |
| 35 | + hittable: true, |
| 36 | + rect: { x: 50, y: 0, width: 100, height: 100 }, |
| 37 | + depth: 0, |
| 38 | +}; |
5 | 39 |
|
6 | 40 | test('parseFindArgs defaults to click with any locator', () => { |
7 | 41 | const parsed = parseFindArgs(['Login']); |
@@ -97,3 +131,81 @@ test('parseFindArgs with bare locator yields empty query', () => { |
97 | 131 | assert.equal(parsed.query, ''); |
98 | 132 | assert.equal(parsed.action, 'click'); |
99 | 133 | }); |
| 134 | + |
| 135 | +test('handleFindCommands click returns deterministic matched-target metadata', async () => { |
| 136 | + const sessionStore = makeSessionStore(); |
| 137 | + const sessionName = 'default'; |
| 138 | + sessionStore.set(sessionName, makeSession(sessionName)); |
| 139 | + |
| 140 | + const invokeCalls: DaemonRequest[] = []; |
| 141 | + const response = await handleFindCommands({ |
| 142 | + req: { |
| 143 | + token: 't', |
| 144 | + session: sessionName, |
| 145 | + command: 'find', |
| 146 | + positionals: ['Increment', 'click'], |
| 147 | + flags: {}, |
| 148 | + }, |
| 149 | + sessionName, |
| 150 | + logPath: '/tmp/test.log', |
| 151 | + sessionStore, |
| 152 | + invoke: async (req) => { |
| 153 | + invokeCalls.push(req); |
| 154 | + // Simulate runner returning non-deterministic platform data that should not bleed through |
| 155 | + return { ok: true, data: { platformSpecificRef: 'XCUIElementTypeApplication', x: 0, y: 0 } }; |
| 156 | + }, |
| 157 | + dispatch: async (_device, command) => { |
| 158 | + if (command === 'snapshot') { |
| 159 | + return { nodes: [INCREMENT_NODE] }; |
| 160 | + } |
| 161 | + return {}; |
| 162 | + }, |
| 163 | + }); |
| 164 | + |
| 165 | + assert.ok(response, 'expected a response'); |
| 166 | + assert.ok(response.ok, 'expected success'); |
| 167 | + const data = response.data as Record<string, unknown>; |
| 168 | + |
| 169 | + // Deterministic matched-target metadata |
| 170 | + assert.equal(data.ref, '@e1', 'ref must match the resolved snapshot node'); |
| 171 | + assert.equal(data.locator, 'any', 'locator must reflect the find strategy'); |
| 172 | + assert.equal(data.query, 'Increment', 'query must reflect the search term'); |
| 173 | + assert.equal(data.x, 100, 'x must be derived from the matched node rect center'); |
| 174 | + assert.equal(data.y, 50, 'y must be derived from the matched node rect center'); |
| 175 | + |
| 176 | + // Non-deterministic platform data must not leak through |
| 177 | + assert.equal(data.platformSpecificRef, undefined, 'platform runner data must not appear in response'); |
| 178 | + |
| 179 | + // invoke was called with the resolved ref |
| 180 | + assert.equal(invokeCalls.length, 1); |
| 181 | + assert.equal(invokeCalls[0].positionals?.[0], '@e1'); |
| 182 | +}); |
| 183 | + |
| 184 | +test('handleFindCommands click with explicit label locator returns locator in metadata', async () => { |
| 185 | + const sessionStore = makeSessionStore(); |
| 186 | + const sessionName = 'default'; |
| 187 | + sessionStore.set(sessionName, makeSession(sessionName)); |
| 188 | + |
| 189 | + const response = await handleFindCommands({ |
| 190 | + req: { |
| 191 | + token: 't', |
| 192 | + session: sessionName, |
| 193 | + command: 'find', |
| 194 | + positionals: ['label', 'Increment', 'click'], |
| 195 | + flags: {}, |
| 196 | + }, |
| 197 | + sessionName, |
| 198 | + logPath: '/tmp/test.log', |
| 199 | + sessionStore, |
| 200 | + invoke: async () => ({ ok: true, data: {} }), |
| 201 | + dispatch: async (_device, command) => { |
| 202 | + if (command === 'snapshot') return { nodes: [INCREMENT_NODE] }; |
| 203 | + return {}; |
| 204 | + }, |
| 205 | + }); |
| 206 | + |
| 207 | + assert.ok(response?.ok); |
| 208 | + const data = response!.data as Record<string, unknown>; |
| 209 | + assert.equal(data.locator, 'label'); |
| 210 | + assert.equal(data.query, 'Increment'); |
| 211 | +}); |
0 commit comments