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.
Files changed (91) hide show
  1. package/README.md +2 -9
  2. package/dist/src/797.js +1 -1
  3. package/dist/src/bin.js +5 -5
  4. package/dist/src/daemon.js +16 -20
  5. package/package.json +7 -9
  6. package/skills/agent-device/SKILL.md +3 -6
  7. package/skills/agent-device/references/permissions.md +3 -15
  8. package/skills/agent-device/references/snapshot-refs.md +1 -4
  9. package/dist/bin/axsnapshot +0 -0
  10. package/ios-runner/AXSnapshot/Package.swift +0 -18
  11. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
  12. package/src/__tests__/cli-close.test.ts +0 -155
  13. package/src/__tests__/cli-help.test.ts +0 -102
  14. package/src/bin.ts +0 -3
  15. package/src/cli.ts +0 -305
  16. package/src/core/__tests__/capabilities.test.ts +0 -75
  17. package/src/core/__tests__/dispatch-open.test.ts +0 -25
  18. package/src/core/__tests__/open-target.test.ts +0 -55
  19. package/src/core/capabilities.ts +0 -57
  20. package/src/core/dispatch.ts +0 -382
  21. package/src/core/open-target.ts +0 -27
  22. package/src/daemon/__tests__/device-ready.test.ts +0 -52
  23. package/src/daemon/__tests__/is-predicates.test.ts +0 -68
  24. package/src/daemon/__tests__/selectors.test.ts +0 -261
  25. package/src/daemon/__tests__/session-routing.test.ts +0 -108
  26. package/src/daemon/__tests__/session-selector.test.ts +0 -64
  27. package/src/daemon/__tests__/session-store.test.ts +0 -142
  28. package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
  29. package/src/daemon/action-utils.ts +0 -29
  30. package/src/daemon/context.ts +0 -48
  31. package/src/daemon/device-ready.ts +0 -155
  32. package/src/daemon/handlers/__tests__/find.test.ts +0 -99
  33. package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
  34. package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
  35. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
  36. package/src/daemon/handlers/__tests__/session.test.ts +0 -820
  37. package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
  38. package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
  39. package/src/daemon/handlers/find.ts +0 -324
  40. package/src/daemon/handlers/interaction.ts +0 -550
  41. package/src/daemon/handlers/parse-utils.ts +0 -8
  42. package/src/daemon/handlers/record-trace.ts +0 -154
  43. package/src/daemon/handlers/session.ts +0 -1137
  44. package/src/daemon/handlers/snapshot.ts +0 -439
  45. package/src/daemon/is-predicates.ts +0 -46
  46. package/src/daemon/selectors.ts +0 -540
  47. package/src/daemon/session-routing.ts +0 -22
  48. package/src/daemon/session-selector.ts +0 -39
  49. package/src/daemon/session-store.ts +0 -296
  50. package/src/daemon/snapshot-processing.ts +0 -131
  51. package/src/daemon/types.ts +0 -56
  52. package/src/daemon-client.ts +0 -272
  53. package/src/daemon.ts +0 -295
  54. package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
  55. package/src/platforms/android/__tests__/index.test.ts +0 -274
  56. package/src/platforms/android/devices.ts +0 -196
  57. package/src/platforms/android/index.ts +0 -784
  58. package/src/platforms/android/ui-hierarchy.ts +0 -312
  59. package/src/platforms/boot-diagnostics.ts +0 -128
  60. package/src/platforms/ios/__tests__/index.test.ts +0 -312
  61. package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
  62. package/src/platforms/ios/apps.ts +0 -358
  63. package/src/platforms/ios/ax-snapshot.ts +0 -207
  64. package/src/platforms/ios/config.ts +0 -28
  65. package/src/platforms/ios/devicectl.ts +0 -134
  66. package/src/platforms/ios/devices.ts +0 -100
  67. package/src/platforms/ios/index.ts +0 -20
  68. package/src/platforms/ios/runner-client.ts +0 -994
  69. package/src/platforms/ios/simulator.ts +0 -164
  70. package/src/utils/__tests__/args.test.ts +0 -239
  71. package/src/utils/__tests__/daemon-client.test.ts +0 -95
  72. package/src/utils/__tests__/exec.test.ts +0 -16
  73. package/src/utils/__tests__/finders.test.ts +0 -34
  74. package/src/utils/__tests__/keyed-lock.test.ts +0 -55
  75. package/src/utils/__tests__/process-identity.test.ts +0 -33
  76. package/src/utils/__tests__/retry.test.ts +0 -44
  77. package/src/utils/args.ts +0 -239
  78. package/src/utils/command-schema.ts +0 -622
  79. package/src/utils/device.ts +0 -84
  80. package/src/utils/errors.ts +0 -35
  81. package/src/utils/exec.ts +0 -339
  82. package/src/utils/finders.ts +0 -101
  83. package/src/utils/interactive.ts +0 -4
  84. package/src/utils/interactors.ts +0 -173
  85. package/src/utils/keyed-lock.ts +0 -14
  86. package/src/utils/output.ts +0 -204
  87. package/src/utils/process-identity.ts +0 -100
  88. package/src/utils/retry.ts +0 -180
  89. package/src/utils/snapshot.ts +0 -64
  90. package/src/utils/timeouts.ts +0 -9
  91. 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
- }
@@ -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
- }