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,261 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import type { SnapshotState } from '../../utils/snapshot.ts';
|
|
4
|
-
import {
|
|
5
|
-
buildSelectorChainForNode,
|
|
6
|
-
findSelectorChainMatch,
|
|
7
|
-
isSelectorToken,
|
|
8
|
-
parseSelectorChain,
|
|
9
|
-
resolveSelectorChain,
|
|
10
|
-
splitSelectorFromArgs,
|
|
11
|
-
} from '../selectors.ts';
|
|
12
|
-
|
|
13
|
-
const nodes: SnapshotState['nodes'] = [
|
|
14
|
-
{
|
|
15
|
-
ref: 'e1',
|
|
16
|
-
index: 0,
|
|
17
|
-
type: 'XCUIElementTypeTextField',
|
|
18
|
-
label: 'Email',
|
|
19
|
-
value: '',
|
|
20
|
-
identifier: 'login_email',
|
|
21
|
-
rect: { x: 0, y: 0, width: 200, height: 44 },
|
|
22
|
-
enabled: true,
|
|
23
|
-
hittable: true,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
ref: 'e2',
|
|
27
|
-
index: 1,
|
|
28
|
-
type: 'XCUIElementTypeButton',
|
|
29
|
-
label: 'Continue',
|
|
30
|
-
identifier: 'auth_continue',
|
|
31
|
-
rect: { x: 0, y: 80, width: 200, height: 44 },
|
|
32
|
-
enabled: true,
|
|
33
|
-
hittable: true,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
ref: 'e3',
|
|
37
|
-
index: 2,
|
|
38
|
-
type: 'XCUIElementTypeButton',
|
|
39
|
-
label: 'Continue',
|
|
40
|
-
identifier: 'secondary_continue',
|
|
41
|
-
rect: { x: 0, y: 140, width: 200, height: 44 },
|
|
42
|
-
enabled: true,
|
|
43
|
-
hittable: true,
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
test('parseSelectorChain parses fallback and boolean terms', () => {
|
|
48
|
-
const chain = parseSelectorChain('id=auth_continue || role=button label="Continue" visible=true');
|
|
49
|
-
assert.equal(chain.selectors.length, 2);
|
|
50
|
-
assert.equal(chain.selectors[0].terms[0].key, 'id');
|
|
51
|
-
assert.equal(chain.selectors[1].terms[2].key, 'visible');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('resolveSelectorChain resolves unique match', () => {
|
|
55
|
-
const chain = parseSelectorChain('id=login_email');
|
|
56
|
-
const resolved = resolveSelectorChain(nodes, chain, {
|
|
57
|
-
platform: 'ios',
|
|
58
|
-
requireRect: true,
|
|
59
|
-
requireUnique: true,
|
|
60
|
-
});
|
|
61
|
-
assert.ok(resolved);
|
|
62
|
-
assert.equal(resolved.node.ref, 'e1');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('resolveSelectorChain falls back when first selector is ambiguous', () => {
|
|
66
|
-
const chain = parseSelectorChain('label="Continue" || id=auth_continue');
|
|
67
|
-
const resolved = resolveSelectorChain(nodes, chain, {
|
|
68
|
-
platform: 'ios',
|
|
69
|
-
requireRect: true,
|
|
70
|
-
requireUnique: true,
|
|
71
|
-
});
|
|
72
|
-
assert.ok(resolved);
|
|
73
|
-
assert.equal(resolved.selectorIndex, 1);
|
|
74
|
-
assert.equal(resolved.node.ref, 'e2');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('resolveSelectorChain keeps strict ambiguity behavior by default', () => {
|
|
78
|
-
const chain = parseSelectorChain('label="Continue"');
|
|
79
|
-
const resolved = resolveSelectorChain(nodes, chain, {
|
|
80
|
-
platform: 'ios',
|
|
81
|
-
requireRect: true,
|
|
82
|
-
requireUnique: true,
|
|
83
|
-
});
|
|
84
|
-
assert.equal(resolved, null);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('resolveSelectorChain disambiguates to deeper/smaller matching node when enabled', () => {
|
|
88
|
-
const disambiguationNodes: SnapshotState['nodes'] = [
|
|
89
|
-
{
|
|
90
|
-
ref: 'e1',
|
|
91
|
-
index: 0,
|
|
92
|
-
type: 'Other',
|
|
93
|
-
label: 'Press me',
|
|
94
|
-
rect: { x: 0, y: 0, width: 300, height: 300 },
|
|
95
|
-
depth: 1,
|
|
96
|
-
enabled: true,
|
|
97
|
-
hittable: true,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
ref: 'e2',
|
|
101
|
-
index: 1,
|
|
102
|
-
type: 'Other',
|
|
103
|
-
label: 'Press me',
|
|
104
|
-
rect: { x: 10, y: 10, width: 100, height: 20 },
|
|
105
|
-
depth: 2,
|
|
106
|
-
enabled: true,
|
|
107
|
-
hittable: true,
|
|
108
|
-
},
|
|
109
|
-
];
|
|
110
|
-
const chain = parseSelectorChain('role="other" label="Press me" || label="Press me"');
|
|
111
|
-
const resolved = resolveSelectorChain(disambiguationNodes, chain, {
|
|
112
|
-
platform: 'ios',
|
|
113
|
-
requireRect: true,
|
|
114
|
-
requireUnique: true,
|
|
115
|
-
disambiguateAmbiguous: true,
|
|
116
|
-
});
|
|
117
|
-
assert.ok(resolved);
|
|
118
|
-
assert.equal(resolved.node.ref, 'e2');
|
|
119
|
-
assert.equal(resolved.matches, 2);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('resolveSelectorChain disambiguation tie falls back to next selector', () => {
|
|
123
|
-
const tieNodes: SnapshotState['nodes'] = [
|
|
124
|
-
{
|
|
125
|
-
ref: 'e1',
|
|
126
|
-
index: 0,
|
|
127
|
-
type: 'Other',
|
|
128
|
-
label: 'Press me',
|
|
129
|
-
rect: { x: 0, y: 0, width: 100, height: 20 },
|
|
130
|
-
depth: 2,
|
|
131
|
-
enabled: true,
|
|
132
|
-
hittable: true,
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
ref: 'e2',
|
|
136
|
-
index: 1,
|
|
137
|
-
type: 'Other',
|
|
138
|
-
label: 'Press me',
|
|
139
|
-
rect: { x: 0, y: 40, width: 100, height: 20 },
|
|
140
|
-
depth: 2,
|
|
141
|
-
enabled: true,
|
|
142
|
-
hittable: true,
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
ref: 'e3',
|
|
146
|
-
index: 2,
|
|
147
|
-
type: 'Other',
|
|
148
|
-
label: 'Press me',
|
|
149
|
-
identifier: 'press_me_unique',
|
|
150
|
-
rect: { x: 0, y: 80, width: 100, height: 20 },
|
|
151
|
-
depth: 2,
|
|
152
|
-
enabled: true,
|
|
153
|
-
hittable: true,
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
const chain = parseSelectorChain('label="Press me" || id="press_me_unique"');
|
|
157
|
-
const resolved = resolveSelectorChain(tieNodes, chain, {
|
|
158
|
-
platform: 'ios',
|
|
159
|
-
requireRect: true,
|
|
160
|
-
requireUnique: true,
|
|
161
|
-
disambiguateAmbiguous: true,
|
|
162
|
-
});
|
|
163
|
-
assert.ok(resolved);
|
|
164
|
-
assert.equal(resolved.selectorIndex, 1);
|
|
165
|
-
assert.equal(resolved.node.ref, 'e3');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('findSelectorChainMatch returns first matching selector for existence checks', () => {
|
|
169
|
-
const chain = parseSelectorChain('label="Continue" || id=auth_continue');
|
|
170
|
-
const match = findSelectorChainMatch(nodes, chain, {
|
|
171
|
-
platform: 'ios',
|
|
172
|
-
});
|
|
173
|
-
assert.ok(match);
|
|
174
|
-
assert.equal(match.selectorIndex, 0);
|
|
175
|
-
assert.equal(match.matches, 2);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('splitSelectorFromArgs extracts selector prefix and trailing value', () => {
|
|
179
|
-
const split = splitSelectorFromArgs(['id=login_email', 'editable=true', 'qa@example.com']);
|
|
180
|
-
assert.ok(split);
|
|
181
|
-
assert.equal(split.selectorExpression, 'id=login_email editable=true');
|
|
182
|
-
assert.deepEqual(split.rest, ['qa@example.com']);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test('splitSelectorFromArgs prefers trailing token for value when requested', () => {
|
|
186
|
-
const split = splitSelectorFromArgs(['label="Filter"', 'visible=true'], { preferTrailingValue: true });
|
|
187
|
-
assert.ok(split);
|
|
188
|
-
assert.equal(split.selectorExpression, 'label="Filter"');
|
|
189
|
-
assert.deepEqual(split.rest, ['visible=true']);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('splitSelectorFromArgs keeps full selector when trailing value preference is disabled', () => {
|
|
193
|
-
const split = splitSelectorFromArgs(['label="Filter"', 'visible=true']);
|
|
194
|
-
assert.ok(split);
|
|
195
|
-
assert.equal(split.selectorExpression, 'label="Filter" visible=true');
|
|
196
|
-
assert.deepEqual(split.rest, []);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('parseSelectorChain rejects unknown keys and malformed quotes', () => {
|
|
200
|
-
assert.throws(() => parseSelectorChain('foo=bar'), /Unknown selector key/i);
|
|
201
|
-
assert.throws(() => parseSelectorChain('label="unclosed'), /Unclosed quote/i);
|
|
202
|
-
assert.throws(() => parseSelectorChain(''), /cannot be empty/i);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('parseSelectorChain handles quoted values ending in escaped backslashes', () => {
|
|
206
|
-
const chain = parseSelectorChain('label="path\\\\" || id=auth_continue');
|
|
207
|
-
assert.equal(chain.selectors.length, 2);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test('isSelectorToken only accepts known keys for key=value tokens', () => {
|
|
211
|
-
assert.equal(isSelectorToken('id=foo'), true);
|
|
212
|
-
assert.equal(isSelectorToken('editable=true'), true);
|
|
213
|
-
assert.equal(isSelectorToken('foo=bar'), false);
|
|
214
|
-
assert.equal(isSelectorToken('a=b'), false);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test('text selector matches extractNodeText semantics (first non-empty field)', () => {
|
|
218
|
-
const chainByLabel = parseSelectorChain('text=Email');
|
|
219
|
-
const chainById = parseSelectorChain('text=login_email');
|
|
220
|
-
const resolvedLabel = resolveSelectorChain(nodes, chainByLabel, {
|
|
221
|
-
platform: 'ios',
|
|
222
|
-
requireUnique: true,
|
|
223
|
-
});
|
|
224
|
-
const resolvedId = resolveSelectorChain(nodes, chainById, {
|
|
225
|
-
platform: 'ios',
|
|
226
|
-
requireUnique: true,
|
|
227
|
-
});
|
|
228
|
-
assert.ok(resolvedLabel);
|
|
229
|
-
assert.equal(resolvedLabel.node.ref, 'e1');
|
|
230
|
-
assert.equal(resolvedId, null);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('buildSelectorChainForNode prefers id and adds editable for fill action', () => {
|
|
234
|
-
const target = nodes[0];
|
|
235
|
-
const chain = buildSelectorChainForNode(target, 'ios', { action: 'fill' });
|
|
236
|
-
assert.ok(chain.some((entry) => entry.includes('id=')));
|
|
237
|
-
assert.ok(chain.some((entry) => entry.includes('editable=true')));
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('role selector normalization matches Android class names by leaf type', () => {
|
|
241
|
-
const androidNodes: SnapshotState['nodes'] = [
|
|
242
|
-
{
|
|
243
|
-
ref: 'a1',
|
|
244
|
-
index: 0,
|
|
245
|
-
type: 'android.widget.Button',
|
|
246
|
-
label: 'Continue',
|
|
247
|
-
identifier: 'auth_continue',
|
|
248
|
-
rect: { x: 0, y: 0, width: 120, height: 44 },
|
|
249
|
-
enabled: true,
|
|
250
|
-
hittable: true,
|
|
251
|
-
},
|
|
252
|
-
];
|
|
253
|
-
const chain = parseSelectorChain('role=button label="Continue"');
|
|
254
|
-
const resolved = resolveSelectorChain(androidNodes, chain, {
|
|
255
|
-
platform: 'android',
|
|
256
|
-
requireRect: true,
|
|
257
|
-
requireUnique: true,
|
|
258
|
-
});
|
|
259
|
-
assert.ok(resolved);
|
|
260
|
-
assert.equal(resolved.node.ref, 'a1');
|
|
261
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { SessionStore } from '../session-store.ts';
|
|
6
|
-
import { resolveEffectiveSessionName } from '../session-routing.ts';
|
|
7
|
-
import type { SessionState } from '../types.ts';
|
|
8
|
-
|
|
9
|
-
function makeSession(name: string): SessionState {
|
|
10
|
-
return {
|
|
11
|
-
name,
|
|
12
|
-
device: {
|
|
13
|
-
platform: 'android',
|
|
14
|
-
id: 'emulator-5554',
|
|
15
|
-
name: 'Pixel',
|
|
16
|
-
kind: 'emulator',
|
|
17
|
-
booted: true,
|
|
18
|
-
},
|
|
19
|
-
createdAt: Date.now(),
|
|
20
|
-
actions: [],
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function makeStore(): SessionStore {
|
|
25
|
-
return new SessionStore(path.join(os.tmpdir(), 'agent-device-session-routing-tests'));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
test('reuses lone active session for implicit default session', () => {
|
|
29
|
-
const store = makeStore();
|
|
30
|
-
store.set('android', makeSession('android'));
|
|
31
|
-
|
|
32
|
-
const resolved = resolveEffectiveSessionName(
|
|
33
|
-
{
|
|
34
|
-
token: 't',
|
|
35
|
-
session: 'default',
|
|
36
|
-
command: 'open',
|
|
37
|
-
positionals: ['com.google.android.apps.maps'],
|
|
38
|
-
flags: {},
|
|
39
|
-
},
|
|
40
|
-
store,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
assert.equal(resolved, 'android');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('keeps requested default when explicit --session is provided', () => {
|
|
47
|
-
const store = makeStore();
|
|
48
|
-
store.set('android', makeSession('android'));
|
|
49
|
-
|
|
50
|
-
const resolved = resolveEffectiveSessionName(
|
|
51
|
-
{
|
|
52
|
-
token: 't',
|
|
53
|
-
session: 'default',
|
|
54
|
-
command: 'open',
|
|
55
|
-
positionals: ['com.google.android.apps.maps'],
|
|
56
|
-
flags: { session: 'default' },
|
|
57
|
-
},
|
|
58
|
-
store,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
assert.equal(resolved, 'default');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('keeps requested non-default session names', () => {
|
|
65
|
-
const store = makeStore();
|
|
66
|
-
store.set('android', makeSession('android'));
|
|
67
|
-
|
|
68
|
-
const resolved = resolveEffectiveSessionName(
|
|
69
|
-
{
|
|
70
|
-
token: 't',
|
|
71
|
-
session: 'maps-test',
|
|
72
|
-
command: 'open',
|
|
73
|
-
positionals: ['com.google.android.apps.maps'],
|
|
74
|
-
flags: {},
|
|
75
|
-
},
|
|
76
|
-
store,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
assert.equal(resolved, 'maps-test');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('does not reuse when multiple sessions are active', () => {
|
|
83
|
-
const store = makeStore();
|
|
84
|
-
store.set('android', makeSession('android'));
|
|
85
|
-
store.set('ios', {
|
|
86
|
-
...makeSession('ios'),
|
|
87
|
-
device: {
|
|
88
|
-
platform: 'ios',
|
|
89
|
-
id: 'ios-sim',
|
|
90
|
-
name: 'iPhone',
|
|
91
|
-
kind: 'simulator',
|
|
92
|
-
booted: true,
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const resolved = resolveEffectiveSessionName(
|
|
97
|
-
{
|
|
98
|
-
token: 't',
|
|
99
|
-
session: 'default',
|
|
100
|
-
command: 'open',
|
|
101
|
-
positionals: ['com.google.android.apps.maps'],
|
|
102
|
-
flags: {},
|
|
103
|
-
},
|
|
104
|
-
store,
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
assert.equal(resolved, 'default');
|
|
108
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { assertSessionSelectorMatches } from '../session-selector.ts';
|
|
4
|
-
import { AppError } from '../../utils/errors.ts';
|
|
5
|
-
import type { SessionState } from '../types.ts';
|
|
6
|
-
|
|
7
|
-
function makeSession(overrides?: Partial<SessionState>): SessionState {
|
|
8
|
-
return {
|
|
9
|
-
name: 'default',
|
|
10
|
-
device: {
|
|
11
|
-
platform: 'android',
|
|
12
|
-
id: 'emulator-5554',
|
|
13
|
-
name: 'Pixel 9',
|
|
14
|
-
kind: 'emulator',
|
|
15
|
-
booted: true,
|
|
16
|
-
},
|
|
17
|
-
createdAt: Date.now(),
|
|
18
|
-
actions: [],
|
|
19
|
-
...overrides,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
test('accepts matching platform and serial selectors', () => {
|
|
24
|
-
const session = makeSession();
|
|
25
|
-
assert.doesNotThrow(() =>
|
|
26
|
-
assertSessionSelectorMatches(session, {
|
|
27
|
-
platform: 'android',
|
|
28
|
-
serial: 'emulator-5554',
|
|
29
|
-
}),
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('rejects mismatched platform selector', () => {
|
|
34
|
-
const session = makeSession();
|
|
35
|
-
assert.throws(
|
|
36
|
-
() => assertSessionSelectorMatches(session, { platform: 'ios' }),
|
|
37
|
-
(err: unknown) =>
|
|
38
|
-
err instanceof AppError &&
|
|
39
|
-
err.code === 'INVALID_ARGS' &&
|
|
40
|
-
err.message.includes('--platform=ios'),
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('rejects mismatched serial selector', () => {
|
|
45
|
-
const session = makeSession();
|
|
46
|
-
assert.throws(
|
|
47
|
-
() => assertSessionSelectorMatches(session, { serial: 'emulator-9999' }),
|
|
48
|
-
(err: unknown) =>
|
|
49
|
-
err instanceof AppError &&
|
|
50
|
-
err.code === 'INVALID_ARGS' &&
|
|
51
|
-
err.message.includes('--serial=emulator-9999'),
|
|
52
|
-
);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('rejects udid selector for android session', () => {
|
|
56
|
-
const session = makeSession();
|
|
57
|
-
assert.throws(
|
|
58
|
-
() => assertSessionSelectorMatches(session, { udid: 'ABC-123' }),
|
|
59
|
-
(err: unknown) =>
|
|
60
|
-
err instanceof AppError &&
|
|
61
|
-
err.code === 'INVALID_ARGS' &&
|
|
62
|
-
err.message.includes('--udid=ABC-123'),
|
|
63
|
-
);
|
|
64
|
-
});
|
|
@@ -1,142 +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 { SessionStore } from '../session-store.ts';
|
|
7
|
-
import type { SessionState } from '../types.ts';
|
|
8
|
-
|
|
9
|
-
function makeSession(name: string): SessionState {
|
|
10
|
-
return {
|
|
11
|
-
name,
|
|
12
|
-
device: {
|
|
13
|
-
platform: 'ios',
|
|
14
|
-
id: 'sim-1',
|
|
15
|
-
name: 'iPhone',
|
|
16
|
-
kind: 'simulator',
|
|
17
|
-
booted: true,
|
|
18
|
-
},
|
|
19
|
-
createdAt: Date.now(),
|
|
20
|
-
actions: [],
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test('recordAction stores normalized action entries', () => {
|
|
25
|
-
const store = new SessionStore(path.join(os.tmpdir(), 'agent-device-tests'));
|
|
26
|
-
const session = makeSession('default');
|
|
27
|
-
store.recordAction(session, {
|
|
28
|
-
command: 'snapshot',
|
|
29
|
-
positionals: [],
|
|
30
|
-
flags: { platform: 'ios', snapshotInteractiveOnly: true, verbose: true },
|
|
31
|
-
result: { nodes: 1 },
|
|
32
|
-
});
|
|
33
|
-
assert.equal(session.actions.length, 1);
|
|
34
|
-
assert.equal(session.actions[0].command, 'snapshot');
|
|
35
|
-
assert.equal(session.actions[0].flags.platform, 'ios');
|
|
36
|
-
assert.equal(session.actions[0].flags.snapshotInteractiveOnly, true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('recordAction skips entries marked noRecord', () => {
|
|
40
|
-
const store = new SessionStore(path.join(os.tmpdir(), 'agent-device-tests'));
|
|
41
|
-
const session = makeSession('default');
|
|
42
|
-
store.recordAction(session, {
|
|
43
|
-
command: 'click',
|
|
44
|
-
positionals: ['@e1'],
|
|
45
|
-
flags: { noRecord: true },
|
|
46
|
-
result: {},
|
|
47
|
-
});
|
|
48
|
-
assert.equal(session.actions.length, 0);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('defaultTracePath sanitizes session name', () => {
|
|
52
|
-
const store = new SessionStore(path.join(os.tmpdir(), 'agent-device-tests'));
|
|
53
|
-
const session = makeSession('session with spaces');
|
|
54
|
-
const tracePath = store.defaultTracePath(session);
|
|
55
|
-
assert.match(tracePath, /session_with_spaces/);
|
|
56
|
-
assert.match(tracePath, /\.trace\.log$/);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('writeSessionLog writes .ad only when recording is enabled', () => {
|
|
60
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-session-log-disabled-'));
|
|
61
|
-
const store = new SessionStore(root);
|
|
62
|
-
const session = makeSession('default');
|
|
63
|
-
store.recordAction(session, {
|
|
64
|
-
command: 'open',
|
|
65
|
-
positionals: ['Settings'],
|
|
66
|
-
flags: { platform: 'ios' },
|
|
67
|
-
result: {},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
store.writeSessionLog(session);
|
|
71
|
-
const files = fs.readdirSync(root);
|
|
72
|
-
assert.equal(files.filter((file) => file.endsWith('.ad')).length, 0);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('saveScript flag enables .ad session log writing', () => {
|
|
76
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-session-log-enabled-'));
|
|
77
|
-
const store = new SessionStore(root);
|
|
78
|
-
const session = makeSession('default');
|
|
79
|
-
store.recordAction(session, {
|
|
80
|
-
command: 'open',
|
|
81
|
-
positionals: ['Settings'],
|
|
82
|
-
flags: { platform: 'ios', saveScript: true },
|
|
83
|
-
result: {},
|
|
84
|
-
});
|
|
85
|
-
store.recordAction(session, {
|
|
86
|
-
command: 'close',
|
|
87
|
-
positionals: [],
|
|
88
|
-
flags: { platform: 'ios' },
|
|
89
|
-
result: {},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
store.writeSessionLog(session);
|
|
93
|
-
const files = fs.readdirSync(root);
|
|
94
|
-
assert.equal(files.filter((file) => file.endsWith('.ad')).length, 1);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('saveScript path writes session log to custom location', () => {
|
|
98
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-session-log-custom-path-'));
|
|
99
|
-
const store = new SessionStore(path.join(root, 'sessions'));
|
|
100
|
-
const session = makeSession('default');
|
|
101
|
-
const customPath = path.join(root, 'workflows', 'my-flow.ad');
|
|
102
|
-
store.recordAction(session, {
|
|
103
|
-
command: 'open',
|
|
104
|
-
positionals: ['Settings'],
|
|
105
|
-
flags: { platform: 'ios', saveScript: customPath },
|
|
106
|
-
result: {},
|
|
107
|
-
});
|
|
108
|
-
store.recordAction(session, {
|
|
109
|
-
command: 'close',
|
|
110
|
-
positionals: [],
|
|
111
|
-
flags: { platform: 'ios' },
|
|
112
|
-
result: {},
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
store.writeSessionLog(session);
|
|
116
|
-
assert.equal(fs.existsSync(customPath), true);
|
|
117
|
-
assert.equal(fs.existsSync(path.join(root, 'sessions')), false);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('writeSessionLog persists open --relaunch in script output', () => {
|
|
121
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-session-log-relaunch-'));
|
|
122
|
-
const store = new SessionStore(root);
|
|
123
|
-
const session = makeSession('default');
|
|
124
|
-
store.recordAction(session, {
|
|
125
|
-
command: 'open',
|
|
126
|
-
positionals: ['Settings'],
|
|
127
|
-
flags: { platform: 'ios', saveScript: true, relaunch: true },
|
|
128
|
-
result: {},
|
|
129
|
-
});
|
|
130
|
-
store.recordAction(session, {
|
|
131
|
-
command: 'close',
|
|
132
|
-
positionals: [],
|
|
133
|
-
flags: { platform: 'ios' },
|
|
134
|
-
result: {},
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
store.writeSessionLog(session);
|
|
138
|
-
const scriptFile = fs.readdirSync(root).find((file) => file.endsWith('.ad'));
|
|
139
|
-
assert.ok(scriptFile);
|
|
140
|
-
const script = fs.readFileSync(path.join(root, scriptFile!), 'utf8');
|
|
141
|
-
assert.match(script, /open "Settings" --relaunch/);
|
|
142
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import test from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { attachRefs } from '../../utils/snapshot.ts';
|
|
4
|
-
import {
|
|
5
|
-
findNearestHittableAncestor,
|
|
6
|
-
isFillableType,
|
|
7
|
-
pruneGroupNodes,
|
|
8
|
-
resolveRefLabel,
|
|
9
|
-
} from '../snapshot-processing.ts';
|
|
10
|
-
|
|
11
|
-
test('pruneGroupNodes drops unlabeled group wrappers and rebalances depth', () => {
|
|
12
|
-
const raw = [
|
|
13
|
-
{ index: 0, depth: 0, type: 'XCUIElementTypeWindow', label: 'Root' },
|
|
14
|
-
{ index: 1, depth: 1, type: 'XCUIElementTypeGroup' },
|
|
15
|
-
{ index: 2, depth: 2, type: 'XCUIElementTypeButton', label: 'Continue' },
|
|
16
|
-
];
|
|
17
|
-
const pruned = pruneGroupNodes(raw);
|
|
18
|
-
assert.equal(pruned.length, 2);
|
|
19
|
-
assert.equal(pruned[1].depth, 1);
|
|
20
|
-
assert.equal(pruned[1].label, 'Continue');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('resolveRefLabel falls back to nearest meaningful neighbor', () => {
|
|
24
|
-
const nodes = attachRefs([
|
|
25
|
-
{ index: 0, depth: 0, label: 'Email', rect: { x: 0, y: 10, width: 100, height: 20 } },
|
|
26
|
-
{ index: 1, depth: 0, label: '', value: '', rect: { x: 0, y: 14, width: 100, height: 20 } },
|
|
27
|
-
]);
|
|
28
|
-
const resolved = resolveRefLabel(nodes[1], nodes);
|
|
29
|
-
assert.equal(resolved, 'Email');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('findNearestHittableAncestor walks parents until hittable node', () => {
|
|
33
|
-
const nodes = attachRefs([
|
|
34
|
-
{ index: 0, parentIndex: undefined, hittable: true, rect: { x: 0, y: 0, width: 100, height: 40 } },
|
|
35
|
-
{ index: 1, parentIndex: 0, hittable: false, rect: { x: 0, y: 0, width: 50, height: 20 } },
|
|
36
|
-
{ index: 2, parentIndex: 1, hittable: false, rect: { x: 0, y: 0, width: 20, height: 20 } },
|
|
37
|
-
]);
|
|
38
|
-
const ancestor = findNearestHittableAncestor(nodes, nodes[2]);
|
|
39
|
-
assert.equal(ancestor?.ref, 'e1');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('isFillableType matches platform-specific editable controls', () => {
|
|
43
|
-
assert.equal(isFillableType('XCUIElementTypeTextField', 'ios'), true);
|
|
44
|
-
assert.equal(isFillableType('XCUIElementTypeButton', 'ios'), false);
|
|
45
|
-
assert.equal(isFillableType('android.widget.EditText', 'android'), true);
|
|
46
|
-
assert.equal(isFillableType('android.widget.Button', 'android'), false);
|
|
47
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { SessionAction } from './types.ts';
|
|
2
|
-
|
|
3
|
-
export function inferFillText(action: SessionAction): string {
|
|
4
|
-
const resultText = action.result?.text;
|
|
5
|
-
if (typeof resultText === 'string' && resultText.trim().length > 0) {
|
|
6
|
-
return resultText;
|
|
7
|
-
}
|
|
8
|
-
const positionals = action.positionals ?? [];
|
|
9
|
-
if (positionals.length === 0) return '';
|
|
10
|
-
if (positionals[0].startsWith('@')) {
|
|
11
|
-
if (positionals.length >= 3) return positionals.slice(2).join(' ').trim();
|
|
12
|
-
return positionals.slice(1).join(' ').trim();
|
|
13
|
-
}
|
|
14
|
-
if (positionals.length >= 3 && !Number.isNaN(Number(positionals[0])) && !Number.isNaN(Number(positionals[1]))) {
|
|
15
|
-
return positionals.slice(2).join(' ').trim();
|
|
16
|
-
}
|
|
17
|
-
return positionals.slice(1).join(' ').trim();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function uniqueStrings(values: string[]): string[] {
|
|
21
|
-
const seen = new Set<string>();
|
|
22
|
-
const output: string[] = [];
|
|
23
|
-
for (const value of values) {
|
|
24
|
-
if (seen.has(value)) continue;
|
|
25
|
-
seen.add(value);
|
|
26
|
-
output.push(value);
|
|
27
|
-
}
|
|
28
|
-
return output;
|
|
29
|
-
}
|
package/src/daemon/context.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { CommandFlags } from '../core/dispatch.ts';
|
|
2
|
-
|
|
3
|
-
export type DaemonCommandContext = {
|
|
4
|
-
appBundleId?: string;
|
|
5
|
-
activity?: string;
|
|
6
|
-
verbose?: boolean;
|
|
7
|
-
logPath?: string;
|
|
8
|
-
traceLogPath?: string;
|
|
9
|
-
snapshotInteractiveOnly?: boolean;
|
|
10
|
-
snapshotCompact?: boolean;
|
|
11
|
-
snapshotDepth?: number;
|
|
12
|
-
snapshotScope?: string;
|
|
13
|
-
snapshotBackend?: 'ax' | 'xctest';
|
|
14
|
-
snapshotRaw?: boolean;
|
|
15
|
-
count?: number;
|
|
16
|
-
intervalMs?: number;
|
|
17
|
-
holdMs?: number;
|
|
18
|
-
jitterPx?: number;
|
|
19
|
-
pauseMs?: number;
|
|
20
|
-
pattern?: 'one-way' | 'ping-pong';
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function contextFromFlags(
|
|
24
|
-
logPath: string,
|
|
25
|
-
flags: CommandFlags | undefined,
|
|
26
|
-
appBundleId?: string,
|
|
27
|
-
traceLogPath?: string,
|
|
28
|
-
): DaemonCommandContext {
|
|
29
|
-
return {
|
|
30
|
-
appBundleId,
|
|
31
|
-
activity: flags?.activity,
|
|
32
|
-
verbose: flags?.verbose,
|
|
33
|
-
logPath,
|
|
34
|
-
traceLogPath,
|
|
35
|
-
snapshotInteractiveOnly: flags?.snapshotInteractiveOnly,
|
|
36
|
-
snapshotCompact: flags?.snapshotCompact,
|
|
37
|
-
snapshotDepth: flags?.snapshotDepth,
|
|
38
|
-
snapshotScope: flags?.snapshotScope,
|
|
39
|
-
snapshotRaw: flags?.snapshotRaw,
|
|
40
|
-
snapshotBackend: flags?.snapshotBackend,
|
|
41
|
-
count: flags?.count,
|
|
42
|
-
intervalMs: flags?.intervalMs,
|
|
43
|
-
holdMs: flags?.holdMs,
|
|
44
|
-
jitterPx: flags?.jitterPx,
|
|
45
|
-
pauseMs: flags?.pauseMs,
|
|
46
|
-
pattern: flags?.pattern,
|
|
47
|
-
};
|
|
48
|
-
}
|