agent-device 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -9
- package/dist/src/797.js +1 -1
- package/dist/src/bin.js +5 -5
- package/dist/src/daemon.js +16 -20
- package/package.json +7 -9
- package/skills/agent-device/SKILL.md +3 -6
- package/skills/agent-device/references/permissions.md +3 -15
- package/skills/agent-device/references/snapshot-refs.md +1 -4
- package/dist/bin/axsnapshot +0 -0
- package/ios-runner/AXSnapshot/Package.swift +0 -18
- package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
- package/src/__tests__/cli-close.test.ts +0 -155
- package/src/__tests__/cli-help.test.ts +0 -102
- package/src/bin.ts +0 -3
- package/src/cli.ts +0 -305
- package/src/core/__tests__/capabilities.test.ts +0 -75
- package/src/core/__tests__/dispatch-open.test.ts +0 -25
- package/src/core/__tests__/open-target.test.ts +0 -55
- package/src/core/capabilities.ts +0 -57
- package/src/core/dispatch.ts +0 -382
- package/src/core/open-target.ts +0 -27
- package/src/daemon/__tests__/device-ready.test.ts +0 -52
- package/src/daemon/__tests__/is-predicates.test.ts +0 -68
- package/src/daemon/__tests__/selectors.test.ts +0 -261
- package/src/daemon/__tests__/session-routing.test.ts +0 -108
- package/src/daemon/__tests__/session-selector.test.ts +0 -64
- package/src/daemon/__tests__/session-store.test.ts +0 -142
- package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
- package/src/daemon/action-utils.ts +0 -29
- package/src/daemon/context.ts +0 -48
- package/src/daemon/device-ready.ts +0 -155
- package/src/daemon/handlers/__tests__/find.test.ts +0 -99
- package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
- package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
- package/src/daemon/handlers/__tests__/session.test.ts +0 -820
- package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
- package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
- package/src/daemon/handlers/find.ts +0 -324
- package/src/daemon/handlers/interaction.ts +0 -550
- package/src/daemon/handlers/parse-utils.ts +0 -8
- package/src/daemon/handlers/record-trace.ts +0 -154
- package/src/daemon/handlers/session.ts +0 -1137
- package/src/daemon/handlers/snapshot.ts +0 -439
- package/src/daemon/is-predicates.ts +0 -46
- package/src/daemon/selectors.ts +0 -540
- package/src/daemon/session-routing.ts +0 -22
- package/src/daemon/session-selector.ts +0 -39
- package/src/daemon/session-store.ts +0 -296
- package/src/daemon/snapshot-processing.ts +0 -131
- package/src/daemon/types.ts +0 -56
- package/src/daemon-client.ts +0 -272
- package/src/daemon.ts +0 -295
- package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
- package/src/platforms/android/__tests__/index.test.ts +0 -274
- package/src/platforms/android/devices.ts +0 -196
- package/src/platforms/android/index.ts +0 -784
- package/src/platforms/android/ui-hierarchy.ts +0 -312
- package/src/platforms/boot-diagnostics.ts +0 -128
- package/src/platforms/ios/__tests__/index.test.ts +0 -312
- package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
- package/src/platforms/ios/apps.ts +0 -358
- package/src/platforms/ios/ax-snapshot.ts +0 -207
- package/src/platforms/ios/config.ts +0 -28
- package/src/platforms/ios/devicectl.ts +0 -134
- package/src/platforms/ios/devices.ts +0 -100
- package/src/platforms/ios/index.ts +0 -20
- package/src/platforms/ios/runner-client.ts +0 -994
- package/src/platforms/ios/simulator.ts +0 -164
- package/src/utils/__tests__/args.test.ts +0 -239
- package/src/utils/__tests__/daemon-client.test.ts +0 -95
- package/src/utils/__tests__/exec.test.ts +0 -16
- package/src/utils/__tests__/finders.test.ts +0 -34
- package/src/utils/__tests__/keyed-lock.test.ts +0 -55
- package/src/utils/__tests__/process-identity.test.ts +0 -33
- package/src/utils/__tests__/retry.test.ts +0 -44
- package/src/utils/args.ts +0 -239
- package/src/utils/command-schema.ts +0 -622
- package/src/utils/device.ts +0 -84
- package/src/utils/errors.ts +0 -35
- package/src/utils/exec.ts +0 -339
- package/src/utils/finders.ts +0 -101
- package/src/utils/interactive.ts +0 -4
- package/src/utils/interactors.ts +0 -173
- package/src/utils/keyed-lock.ts +0 -14
- package/src/utils/output.ts +0 -204
- package/src/utils/process-identity.ts +0 -100
- package/src/utils/retry.ts +0 -180
- package/src/utils/snapshot.ts +0 -64
- package/src/utils/timeouts.ts +0 -9
- package/src/utils/version.ts +0 -26
|
@@ -1,509 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import type { CommandFlags } from '../../../core/dispatch.ts';
|
|
7
|
-
import { handleSessionCommands } from '../session.ts';
|
|
8
|
-
import { SessionStore } from '../../session-store.ts';
|
|
9
|
-
import type { DaemonRequest, DaemonResponse, SessionAction, SessionState } from '../../types.ts';
|
|
10
|
-
import type { DeviceInfo } from '../../../utils/device.ts';
|
|
11
|
-
|
|
12
|
-
function makeDevice(): DeviceInfo {
|
|
13
|
-
return {
|
|
14
|
-
platform: 'ios',
|
|
15
|
-
id: 'sim-1',
|
|
16
|
-
name: 'iPhone Test',
|
|
17
|
-
kind: 'simulator',
|
|
18
|
-
booted: true,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function makeSession(name: string): SessionState {
|
|
23
|
-
return {
|
|
24
|
-
name,
|
|
25
|
-
device: makeDevice(),
|
|
26
|
-
createdAt: Date.now(),
|
|
27
|
-
appBundleId: 'com.example.app',
|
|
28
|
-
actions: [],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function writeReplayFile(filePath: string, action: SessionAction) {
|
|
33
|
-
const args = action.positionals.map((value) => JSON.stringify(value)).join(' ');
|
|
34
|
-
fs.writeFileSync(filePath, `${action.command}${args.length > 0 ? ` ${args}` : ''}\n`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function readReplaySelector(filePath: string, command: string): string {
|
|
38
|
-
const lines = fs
|
|
39
|
-
.readFileSync(filePath, 'utf8')
|
|
40
|
-
.split(/\r?\n/)
|
|
41
|
-
.map((line) => line.trim())
|
|
42
|
-
.filter((line) => line.length > 0);
|
|
43
|
-
const line = lines.find((entry) => entry.startsWith(`${command} `) || entry === command);
|
|
44
|
-
if (!line) return '';
|
|
45
|
-
const args = tokenizeReplayLine(line).slice(1);
|
|
46
|
-
if (command === 'is') {
|
|
47
|
-
return args[1] ?? '';
|
|
48
|
-
}
|
|
49
|
-
return args[0] ?? '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function tokenizeReplayLine(line: string): string[] {
|
|
53
|
-
const tokens: string[] = [];
|
|
54
|
-
let cursor = 0;
|
|
55
|
-
while (cursor < line.length) {
|
|
56
|
-
while (cursor < line.length && /\s/.test(line[cursor])) {
|
|
57
|
-
cursor += 1;
|
|
58
|
-
}
|
|
59
|
-
if (cursor >= line.length) break;
|
|
60
|
-
if (line[cursor] === '"') {
|
|
61
|
-
let end = cursor + 1;
|
|
62
|
-
let escaped = false;
|
|
63
|
-
while (end < line.length) {
|
|
64
|
-
const char = line[end];
|
|
65
|
-
if (char === '"' && !escaped) break;
|
|
66
|
-
escaped = char === '\\' && !escaped;
|
|
67
|
-
if (char !== '\\') escaped = false;
|
|
68
|
-
end += 1;
|
|
69
|
-
}
|
|
70
|
-
if (end >= line.length) {
|
|
71
|
-
throw new Error(`Invalid replay script line: ${line}`);
|
|
72
|
-
}
|
|
73
|
-
tokens.push(JSON.parse(line.slice(cursor, end + 1)) as string);
|
|
74
|
-
cursor = end + 1;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
let end = cursor;
|
|
78
|
-
while (end < line.length && !/\s/.test(line[end])) {
|
|
79
|
-
end += 1;
|
|
80
|
-
}
|
|
81
|
-
tokens.push(line.slice(cursor, end));
|
|
82
|
-
cursor = end;
|
|
83
|
-
}
|
|
84
|
-
return tokens;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
test('replay --update heals selector and rewrites replay file', async () => {
|
|
88
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-heal-'));
|
|
89
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
90
|
-
const replayPath = path.join(tempRoot, 'replay.ad');
|
|
91
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
92
|
-
const sessionName = 'heal-session';
|
|
93
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
94
|
-
|
|
95
|
-
writeReplayFile(replayPath, {
|
|
96
|
-
ts: Date.now(),
|
|
97
|
-
command: 'click',
|
|
98
|
-
positionals: ['id="old_continue" || label="Continue"'],
|
|
99
|
-
flags: {},
|
|
100
|
-
result: {},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const invokeCalls: string[] = [];
|
|
104
|
-
const invoke = async (request: DaemonRequest): Promise<DaemonResponse> => {
|
|
105
|
-
if (request.command !== 'click') {
|
|
106
|
-
return { ok: false, error: { code: 'INVALID_ARGS', message: `unexpected command ${request.command}` } };
|
|
107
|
-
}
|
|
108
|
-
const selector = request.positionals?.[0] ?? '';
|
|
109
|
-
invokeCalls.push(selector);
|
|
110
|
-
if (selector.includes('old_continue')) {
|
|
111
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'selector no longer exists' } };
|
|
112
|
-
}
|
|
113
|
-
if (selector.includes('auth_continue')) {
|
|
114
|
-
return { ok: true, data: { clicked: true } };
|
|
115
|
-
}
|
|
116
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'unexpected selector' } };
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
let snapshotDispatchCalls = 0;
|
|
120
|
-
const dispatch = async (
|
|
121
|
-
_device: DeviceInfo,
|
|
122
|
-
command: string,
|
|
123
|
-
_positionals: string[],
|
|
124
|
-
_out?: string,
|
|
125
|
-
_context?: CommandFlags,
|
|
126
|
-
): Promise<Record<string, unknown> | void> => {
|
|
127
|
-
if (command !== 'snapshot') {
|
|
128
|
-
throw new Error(`unexpected dispatch command: ${command}`);
|
|
129
|
-
}
|
|
130
|
-
snapshotDispatchCalls += 1;
|
|
131
|
-
return {
|
|
132
|
-
nodes: [
|
|
133
|
-
{
|
|
134
|
-
index: 0,
|
|
135
|
-
type: 'XCUIElementTypeButton',
|
|
136
|
-
label: 'Continue',
|
|
137
|
-
identifier: 'auth_continue',
|
|
138
|
-
rect: { x: 10, y: 10, width: 100, height: 44 },
|
|
139
|
-
enabled: true,
|
|
140
|
-
hittable: true,
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
truncated: false,
|
|
144
|
-
backend: 'xctest',
|
|
145
|
-
};
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const response = await handleSessionCommands({
|
|
149
|
-
req: {
|
|
150
|
-
token: 't',
|
|
151
|
-
session: sessionName,
|
|
152
|
-
command: 'replay',
|
|
153
|
-
positionals: [replayPath],
|
|
154
|
-
flags: { replayUpdate: true },
|
|
155
|
-
},
|
|
156
|
-
sessionName,
|
|
157
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
158
|
-
sessionStore,
|
|
159
|
-
invoke,
|
|
160
|
-
dispatch,
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
assert.ok(response);
|
|
164
|
-
assert.equal(response.ok, true, JSON.stringify(response));
|
|
165
|
-
if (response.ok) {
|
|
166
|
-
assert.equal(response.data?.healed, 1);
|
|
167
|
-
assert.equal(response.data?.replayed, 1);
|
|
168
|
-
}
|
|
169
|
-
assert.equal(snapshotDispatchCalls, 1);
|
|
170
|
-
assert.equal(invokeCalls.length, 2);
|
|
171
|
-
assert.ok(invokeCalls[0].includes('old_continue'));
|
|
172
|
-
assert.ok(invokeCalls[1].includes('auth_continue'));
|
|
173
|
-
const rewrittenSelector = readReplaySelector(replayPath, 'click');
|
|
174
|
-
assert.ok(rewrittenSelector.includes('auth_continue'));
|
|
175
|
-
assert.ok(!rewrittenSelector.includes('old_continue'));
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('replay without --update does not heal or rewrite', async () => {
|
|
179
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-noheal-'));
|
|
180
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
181
|
-
const replayPath = path.join(tempRoot, 'replay.ad');
|
|
182
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
183
|
-
const sessionName = 'noheal-session';
|
|
184
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
185
|
-
|
|
186
|
-
writeReplayFile(replayPath, {
|
|
187
|
-
ts: Date.now(),
|
|
188
|
-
command: 'click',
|
|
189
|
-
positionals: ['id="old_continue" || label="Continue"'],
|
|
190
|
-
flags: {},
|
|
191
|
-
result: {},
|
|
192
|
-
});
|
|
193
|
-
const originalPayload = fs.readFileSync(replayPath, 'utf8');
|
|
194
|
-
|
|
195
|
-
const invoke = async (_request: DaemonRequest): Promise<DaemonResponse> => {
|
|
196
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'selector no longer exists' } };
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
let snapshotDispatchCalls = 0;
|
|
200
|
-
const dispatch = async (
|
|
201
|
-
_device: DeviceInfo,
|
|
202
|
-
_command: string,
|
|
203
|
-
_positionals: string[],
|
|
204
|
-
_out?: string,
|
|
205
|
-
_context?: CommandFlags,
|
|
206
|
-
): Promise<Record<string, unknown> | void> => {
|
|
207
|
-
snapshotDispatchCalls += 1;
|
|
208
|
-
return {};
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const response = await handleSessionCommands({
|
|
212
|
-
req: {
|
|
213
|
-
token: 't',
|
|
214
|
-
session: sessionName,
|
|
215
|
-
command: 'replay',
|
|
216
|
-
positionals: [replayPath],
|
|
217
|
-
flags: {},
|
|
218
|
-
},
|
|
219
|
-
sessionName,
|
|
220
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
221
|
-
sessionStore,
|
|
222
|
-
invoke,
|
|
223
|
-
dispatch,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
assert.ok(response);
|
|
227
|
-
assert.equal(response.ok, false);
|
|
228
|
-
if (!response.ok) {
|
|
229
|
-
assert.match(response.error.message, /Replay failed at step 1/);
|
|
230
|
-
assert.equal(response.error.details?.step, 1);
|
|
231
|
-
assert.equal(response.error.details?.action, 'click');
|
|
232
|
-
}
|
|
233
|
-
assert.equal(snapshotDispatchCalls, 0);
|
|
234
|
-
assert.equal(fs.readFileSync(replayPath, 'utf8'), originalPayload);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
test('replay --update skips malformed selector candidates and preserves replay error context', async () => {
|
|
238
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-malformed-candidate-'));
|
|
239
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
240
|
-
const replayPath = path.join(tempRoot, 'replay.ad');
|
|
241
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
242
|
-
const sessionName = 'malformed-candidate-session';
|
|
243
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
244
|
-
|
|
245
|
-
writeReplayFile(replayPath, {
|
|
246
|
-
ts: Date.now(),
|
|
247
|
-
command: 'click',
|
|
248
|
-
positionals: ['id="old_continue" ||'],
|
|
249
|
-
flags: {},
|
|
250
|
-
result: {},
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const dispatch = async (): Promise<Record<string, unknown> | void> => {
|
|
254
|
-
return {
|
|
255
|
-
nodes: [
|
|
256
|
-
{
|
|
257
|
-
index: 0,
|
|
258
|
-
type: 'XCUIElementTypeButton',
|
|
259
|
-
label: 'Continue',
|
|
260
|
-
identifier: 'auth_continue',
|
|
261
|
-
rect: { x: 10, y: 10, width: 100, height: 44 },
|
|
262
|
-
enabled: true,
|
|
263
|
-
hittable: true,
|
|
264
|
-
},
|
|
265
|
-
],
|
|
266
|
-
truncated: false,
|
|
267
|
-
backend: 'xctest',
|
|
268
|
-
};
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
const response = await handleSessionCommands({
|
|
272
|
-
req: {
|
|
273
|
-
token: 't',
|
|
274
|
-
session: sessionName,
|
|
275
|
-
command: 'replay',
|
|
276
|
-
positionals: [replayPath],
|
|
277
|
-
flags: { replayUpdate: true },
|
|
278
|
-
},
|
|
279
|
-
sessionName,
|
|
280
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
281
|
-
sessionStore,
|
|
282
|
-
invoke: async () => ({ ok: false, error: { code: 'COMMAND_FAILED', message: 'selector stale' } }),
|
|
283
|
-
dispatch,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
assert.ok(response);
|
|
287
|
-
assert.equal(response.ok, false);
|
|
288
|
-
if (!response.ok) {
|
|
289
|
-
assert.equal(response.error.code, 'COMMAND_FAILED');
|
|
290
|
-
assert.match(response.error.message, /Replay failed at step 1/);
|
|
291
|
-
assert.equal(response.error.details?.step, 1);
|
|
292
|
-
assert.equal(response.error.details?.action, 'click');
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('replay --update heals selector in is command', async () => {
|
|
297
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-heal-is-'));
|
|
298
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
299
|
-
const replayPath = path.join(tempRoot, 'replay.ad');
|
|
300
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
301
|
-
const sessionName = 'heal-is-session';
|
|
302
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
303
|
-
|
|
304
|
-
writeReplayFile(replayPath, {
|
|
305
|
-
ts: Date.now(),
|
|
306
|
-
command: 'is',
|
|
307
|
-
positionals: ['visible', 'id="old_continue" || label="Continue"'],
|
|
308
|
-
flags: {},
|
|
309
|
-
result: {},
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
const invoke = async (request: DaemonRequest): Promise<DaemonResponse> => {
|
|
313
|
-
if (request.command !== 'is') {
|
|
314
|
-
return { ok: false, error: { code: 'INVALID_ARGS', message: `unexpected command ${request.command}` } };
|
|
315
|
-
}
|
|
316
|
-
const selector = request.positionals?.[1] ?? '';
|
|
317
|
-
if (selector.includes('old_continue')) {
|
|
318
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'selector stale' } };
|
|
319
|
-
}
|
|
320
|
-
if (selector.includes('auth_continue')) {
|
|
321
|
-
return { ok: true, data: { predicate: 'visible', pass: true } };
|
|
322
|
-
}
|
|
323
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'unexpected selector' } };
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const dispatch = async (): Promise<Record<string, unknown> | void> => {
|
|
327
|
-
return {
|
|
328
|
-
nodes: [
|
|
329
|
-
{
|
|
330
|
-
index: 0,
|
|
331
|
-
type: 'XCUIElementTypeButton',
|
|
332
|
-
label: 'Continue',
|
|
333
|
-
identifier: 'auth_continue',
|
|
334
|
-
rect: { x: 10, y: 10, width: 100, height: 44 },
|
|
335
|
-
enabled: true,
|
|
336
|
-
hittable: true,
|
|
337
|
-
},
|
|
338
|
-
],
|
|
339
|
-
truncated: false,
|
|
340
|
-
backend: 'xctest',
|
|
341
|
-
};
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const response = await handleSessionCommands({
|
|
345
|
-
req: {
|
|
346
|
-
token: 't',
|
|
347
|
-
session: sessionName,
|
|
348
|
-
command: 'replay',
|
|
349
|
-
positionals: [replayPath],
|
|
350
|
-
flags: { replayUpdate: true },
|
|
351
|
-
},
|
|
352
|
-
sessionName,
|
|
353
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
354
|
-
sessionStore,
|
|
355
|
-
invoke,
|
|
356
|
-
dispatch,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
assert.ok(response);
|
|
360
|
-
assert.equal(response.ok, true, JSON.stringify(response));
|
|
361
|
-
if (response.ok) {
|
|
362
|
-
assert.equal(response.data?.healed, 1);
|
|
363
|
-
}
|
|
364
|
-
const rewrittenSelector = readReplaySelector(replayPath, 'is');
|
|
365
|
-
assert.ok(rewrittenSelector.includes('auth_continue'));
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test('replay --update heals numeric get text drift when numeric candidate value is unique', async () => {
|
|
369
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-heal-get-numeric-'));
|
|
370
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
371
|
-
const replayPath = path.join(tempRoot, 'replay.ad');
|
|
372
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
373
|
-
const sessionName = 'heal-get-numeric-session';
|
|
374
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
375
|
-
|
|
376
|
-
writeReplayFile(replayPath, {
|
|
377
|
-
ts: Date.now(),
|
|
378
|
-
command: 'get',
|
|
379
|
-
positionals: ['text', 'role="statictext" label="2" || label="2"'],
|
|
380
|
-
flags: {},
|
|
381
|
-
result: {},
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const invokeCalls: string[] = [];
|
|
385
|
-
const invoke = async (request: DaemonRequest): Promise<DaemonResponse> => {
|
|
386
|
-
if (request.command !== 'get') {
|
|
387
|
-
return { ok: false, error: { code: 'INVALID_ARGS', message: `unexpected command ${request.command}` } };
|
|
388
|
-
}
|
|
389
|
-
const selector = request.positionals?.[1] ?? '';
|
|
390
|
-
invokeCalls.push(selector);
|
|
391
|
-
if (selector.includes('label="2"')) {
|
|
392
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'selector stale' } };
|
|
393
|
-
}
|
|
394
|
-
if (selector.includes('label="20"')) {
|
|
395
|
-
return { ok: true, data: { text: '20' } };
|
|
396
|
-
}
|
|
397
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'unexpected selector' } };
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
const dispatch = async (): Promise<Record<string, unknown> | void> => {
|
|
401
|
-
return {
|
|
402
|
-
nodes: [
|
|
403
|
-
{
|
|
404
|
-
index: 0,
|
|
405
|
-
type: 'XCUIElementTypeStaticText',
|
|
406
|
-
label: '20',
|
|
407
|
-
rect: { x: 0, y: 100, width: 100, height: 24 },
|
|
408
|
-
enabled: true,
|
|
409
|
-
hittable: true,
|
|
410
|
-
},
|
|
411
|
-
{
|
|
412
|
-
index: 1,
|
|
413
|
-
type: 'XCUIElementTypeStaticText',
|
|
414
|
-
label: 'Version: 0.84.0',
|
|
415
|
-
rect: { x: 0, y: 200, width: 220, height: 17 },
|
|
416
|
-
enabled: true,
|
|
417
|
-
hittable: true,
|
|
418
|
-
},
|
|
419
|
-
],
|
|
420
|
-
truncated: false,
|
|
421
|
-
backend: 'xctest',
|
|
422
|
-
};
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const response = await handleSessionCommands({
|
|
426
|
-
req: {
|
|
427
|
-
token: 't',
|
|
428
|
-
session: sessionName,
|
|
429
|
-
command: 'replay',
|
|
430
|
-
positionals: [replayPath],
|
|
431
|
-
flags: { replayUpdate: true },
|
|
432
|
-
},
|
|
433
|
-
sessionName,
|
|
434
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
435
|
-
sessionStore,
|
|
436
|
-
invoke,
|
|
437
|
-
dispatch,
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
assert.ok(response);
|
|
441
|
-
assert.equal(response.ok, true, JSON.stringify(response));
|
|
442
|
-
if (response.ok) {
|
|
443
|
-
assert.equal(response.data?.healed, 1);
|
|
444
|
-
assert.equal(response.data?.replayed, 1);
|
|
445
|
-
}
|
|
446
|
-
assert.equal(invokeCalls.length, 2);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
test('replay rejects legacy JSON payload files', async () => {
|
|
450
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-json-rejected-'));
|
|
451
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
452
|
-
const replayPath = path.join(tempRoot, 'replay.json');
|
|
453
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
454
|
-
const sessionName = 'json-rejected-session';
|
|
455
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
456
|
-
fs.writeFileSync(replayPath, JSON.stringify({ optimizedActions: [] }, null, 2));
|
|
457
|
-
|
|
458
|
-
const response = await handleSessionCommands({
|
|
459
|
-
req: {
|
|
460
|
-
token: 't',
|
|
461
|
-
session: sessionName,
|
|
462
|
-
command: 'replay',
|
|
463
|
-
positionals: [replayPath],
|
|
464
|
-
flags: {},
|
|
465
|
-
},
|
|
466
|
-
sessionName,
|
|
467
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
468
|
-
sessionStore,
|
|
469
|
-
invoke: async () => ({ ok: true, data: {} }),
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
assert.ok(response);
|
|
473
|
-
assert.equal(response.ok, false);
|
|
474
|
-
if (!response.ok) {
|
|
475
|
-
assert.equal(response.error.code, 'INVALID_ARGS');
|
|
476
|
-
assert.match(response.error.message, /\.ad script files/);
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
test('replay rejects malformed .ad lines with unclosed quotes', async () => {
|
|
481
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-replay-invalid-ad-'));
|
|
482
|
-
const sessionsDir = path.join(tempRoot, 'sessions');
|
|
483
|
-
const replayPath = path.join(tempRoot, 'replay.ad');
|
|
484
|
-
const sessionStore = new SessionStore(sessionsDir);
|
|
485
|
-
const sessionName = 'invalid-ad-session';
|
|
486
|
-
sessionStore.set(sessionName, makeSession(sessionName));
|
|
487
|
-
fs.writeFileSync(replayPath, 'click "id=\\"broken\\"\n');
|
|
488
|
-
|
|
489
|
-
const response = await handleSessionCommands({
|
|
490
|
-
req: {
|
|
491
|
-
token: 't',
|
|
492
|
-
session: sessionName,
|
|
493
|
-
command: 'replay',
|
|
494
|
-
positionals: [replayPath],
|
|
495
|
-
flags: {},
|
|
496
|
-
},
|
|
497
|
-
sessionName,
|
|
498
|
-
logPath: path.join(tempRoot, 'daemon.log'),
|
|
499
|
-
sessionStore,
|
|
500
|
-
invoke: async () => ({ ok: true, data: {} }),
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
assert.ok(response);
|
|
504
|
-
assert.equal(response.ok, false);
|
|
505
|
-
if (!response.ok) {
|
|
506
|
-
assert.equal(response.error.code, 'INVALID_ARGS');
|
|
507
|
-
assert.match(response.error.message, /Invalid replay script line/);
|
|
508
|
-
}
|
|
509
|
-
});
|