agent-device 0.4.1 → 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 (87) hide show
  1. package/README.md +19 -20
  2. package/dist/src/797.js +1 -1
  3. package/dist/src/bin.js +32 -32
  4. package/dist/src/daemon.js +17 -17
  5. package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +8 -2
  6. package/package.json +7 -9
  7. package/skills/agent-device/SKILL.md +22 -19
  8. package/skills/agent-device/references/permissions.md +10 -17
  9. package/skills/agent-device/references/session-management.md +3 -1
  10. package/skills/agent-device/references/snapshot-refs.md +1 -4
  11. package/dist/bin/axsnapshot +0 -0
  12. package/ios-runner/AXSnapshot/Package.swift +0 -18
  13. package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
  14. package/src/__tests__/cli-help.test.ts +0 -102
  15. package/src/bin.ts +0 -3
  16. package/src/cli.ts +0 -289
  17. package/src/core/__tests__/capabilities.test.ts +0 -74
  18. package/src/core/__tests__/open-target.test.ts +0 -16
  19. package/src/core/capabilities.ts +0 -57
  20. package/src/core/dispatch.ts +0 -360
  21. package/src/core/open-target.ts +0 -13
  22. package/src/daemon/__tests__/app-state.test.ts +0 -138
  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/app-state.ts +0 -65
  31. package/src/daemon/context.ts +0 -48
  32. package/src/daemon/device-ready.ts +0 -13
  33. package/src/daemon/handlers/__tests__/find.test.ts +0 -99
  34. package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
  35. package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
  36. package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
  37. package/src/daemon/handlers/__tests__/session.test.ts +0 -343
  38. package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
  39. package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
  40. package/src/daemon/handlers/find.ts +0 -324
  41. package/src/daemon/handlers/interaction.ts +0 -550
  42. package/src/daemon/handlers/parse-utils.ts +0 -8
  43. package/src/daemon/handlers/record-trace.ts +0 -154
  44. package/src/daemon/handlers/session.ts +0 -1032
  45. package/src/daemon/handlers/snapshot.ts +0 -439
  46. package/src/daemon/is-predicates.ts +0 -46
  47. package/src/daemon/selectors.ts +0 -540
  48. package/src/daemon/session-routing.ts +0 -22
  49. package/src/daemon/session-selector.ts +0 -39
  50. package/src/daemon/session-store.ts +0 -298
  51. package/src/daemon/snapshot-processing.ts +0 -131
  52. package/src/daemon/types.ts +0 -56
  53. package/src/daemon-client.ts +0 -172
  54. package/src/daemon.ts +0 -295
  55. package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
  56. package/src/platforms/android/__tests__/index.test.ts +0 -157
  57. package/src/platforms/android/devices.ts +0 -196
  58. package/src/platforms/android/index.ts +0 -754
  59. package/src/platforms/android/ui-hierarchy.ts +0 -312
  60. package/src/platforms/boot-diagnostics.ts +0 -128
  61. package/src/platforms/ios/__tests__/index.test.ts +0 -24
  62. package/src/platforms/ios/__tests__/runner-client.test.ts +0 -113
  63. package/src/platforms/ios/ax-snapshot.ts +0 -207
  64. package/src/platforms/ios/devices.ts +0 -87
  65. package/src/platforms/ios/index.ts +0 -455
  66. package/src/platforms/ios/runner-client.ts +0 -938
  67. package/src/utils/__tests__/args.test.ts +0 -221
  68. package/src/utils/__tests__/daemon-client.test.ts +0 -78
  69. package/src/utils/__tests__/exec.test.ts +0 -16
  70. package/src/utils/__tests__/finders.test.ts +0 -34
  71. package/src/utils/__tests__/keyed-lock.test.ts +0 -55
  72. package/src/utils/__tests__/process-identity.test.ts +0 -33
  73. package/src/utils/__tests__/retry.test.ts +0 -44
  74. package/src/utils/args.ts +0 -234
  75. package/src/utils/command-schema.ts +0 -629
  76. package/src/utils/device.ts +0 -84
  77. package/src/utils/errors.ts +0 -35
  78. package/src/utils/exec.ts +0 -339
  79. package/src/utils/finders.ts +0 -101
  80. package/src/utils/interactive.ts +0 -4
  81. package/src/utils/interactors.ts +0 -173
  82. package/src/utils/keyed-lock.ts +0 -14
  83. package/src/utils/output.ts +0 -204
  84. package/src/utils/process-identity.ts +0 -100
  85. package/src/utils/retry.ts +0 -180
  86. package/src/utils/snapshot.ts +0 -64
  87. package/src/utils/version.ts +0 -26
@@ -1,312 +0,0 @@
1
- import type { RawSnapshotNode, Rect, SnapshotOptions } from '../../utils/snapshot.ts';
2
-
3
- export function findBounds(xml: string, query: string): { x: number; y: number } | null {
4
- const q = query.toLowerCase();
5
- const nodeRegex = /<node[^>]+>/g;
6
- let match = nodeRegex.exec(xml);
7
- while (match) {
8
- const node = match[0];
9
- const attrs = parseXmlNodeAttributes(node);
10
- const textVal = (readXmlAttr(attrs, 'text') ?? '').toLowerCase();
11
- const descVal = (readXmlAttr(attrs, 'content-desc') ?? '').toLowerCase();
12
- if (textVal.includes(q) || descVal.includes(q)) {
13
- const rect = parseBounds(readXmlAttr(attrs, 'bounds'));
14
- if (rect) {
15
- return {
16
- x: Math.floor(rect.x + rect.width / 2),
17
- y: Math.floor(rect.y + rect.height / 2),
18
- };
19
- }
20
- return { x: 0, y: 0 };
21
- }
22
- match = nodeRegex.exec(xml);
23
- }
24
- return null;
25
- }
26
-
27
- export function parseUiHierarchy(
28
- xml: string,
29
- maxNodes: number,
30
- options: SnapshotOptions,
31
- ): { nodes: RawSnapshotNode[]; truncated?: boolean } {
32
- const tree = parseUiHierarchyTree(xml);
33
- const nodes: RawSnapshotNode[] = [];
34
- let truncated = false;
35
- const maxDepth = options.depth ?? Number.POSITIVE_INFINITY;
36
- const scopedRoot = options.scope ? findScopeNode(tree, options.scope) : null;
37
- const roots = scopedRoot ? [scopedRoot] : tree.children;
38
-
39
- const interactiveDescendantMemo = new Map<AndroidNode, boolean>();
40
- const hasInteractiveDescendant = (node: AndroidNode): boolean => {
41
- const cached = interactiveDescendantMemo.get(node);
42
- if (cached !== undefined) return cached;
43
- for (const child of node.children) {
44
- if (child.hittable || hasInteractiveDescendant(child)) {
45
- interactiveDescendantMemo.set(node, true);
46
- return true;
47
- }
48
- }
49
- interactiveDescendantMemo.set(node, false);
50
- return false;
51
- };
52
-
53
- const walk = (
54
- node: AndroidNode,
55
- depth: number,
56
- parentIndex?: number,
57
- ancestorHittable: boolean = false,
58
- ancestorCollection: boolean = false,
59
- ) => {
60
- if (nodes.length >= maxNodes) {
61
- truncated = true;
62
- return;
63
- }
64
- if (depth > maxDepth) return;
65
-
66
- const include = options.raw
67
- ? true
68
- : shouldIncludeAndroidNode(
69
- node,
70
- options,
71
- ancestorHittable,
72
- hasInteractiveDescendant(node),
73
- ancestorCollection,
74
- );
75
- let currentIndex = parentIndex;
76
- if (include) {
77
- currentIndex = nodes.length;
78
- nodes.push({
79
- index: currentIndex,
80
- type: node.type ?? undefined,
81
- label: node.label ?? undefined,
82
- value: node.value ?? undefined,
83
- identifier: node.identifier ?? undefined,
84
- rect: node.rect,
85
- enabled: node.enabled,
86
- hittable: node.hittable,
87
- depth,
88
- parentIndex,
89
- });
90
- }
91
- const nextAncestorHittable = ancestorHittable || Boolean(node.hittable);
92
- const nextAncestorCollection = ancestorCollection || isCollectionContainerType(node.type);
93
- for (const child of node.children) {
94
- walk(child, depth + 1, currentIndex, nextAncestorHittable, nextAncestorCollection);
95
- if (truncated) return;
96
- }
97
- };
98
-
99
- for (const root of roots) {
100
- walk(root, 0, undefined, false, false);
101
- if (truncated) break;
102
- }
103
-
104
- return truncated ? { nodes, truncated } : { nodes };
105
- }
106
-
107
- export function readNodeAttributes(node: string): {
108
- text: string | null;
109
- desc: string | null;
110
- resourceId: string | null;
111
- className: string | null;
112
- bounds: string | null;
113
- clickable?: boolean;
114
- enabled?: boolean;
115
- focusable?: boolean;
116
- focused?: boolean;
117
- } {
118
- const attrs = parseXmlNodeAttributes(node);
119
- const getAttr = (name: string): string | null => readXmlAttr(attrs, name);
120
- const boolAttr = (name: string): boolean | undefined => {
121
- const raw = getAttr(name);
122
- if (raw === null) return undefined;
123
- return raw === 'true';
124
- };
125
- return {
126
- text: getAttr('text'),
127
- desc: getAttr('content-desc'),
128
- resourceId: getAttr('resource-id'),
129
- className: getAttr('class'),
130
- bounds: getAttr('bounds'),
131
- clickable: boolAttr('clickable'),
132
- enabled: boolAttr('enabled'),
133
- focusable: boolAttr('focusable'),
134
- focused: boolAttr('focused'),
135
- };
136
- }
137
-
138
- function parseXmlNodeAttributes(node: string): Map<string, string> {
139
- const attrs = new Map<string, string>();
140
- const start = node.indexOf(' ');
141
- const end = node.lastIndexOf('>');
142
- if (start < 0 || end <= start) return attrs;
143
-
144
- const attrRegex = /([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/y;
145
- let cursor = start;
146
- while (cursor < end) {
147
- while (cursor < end) {
148
- const char = node[cursor];
149
- if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') break;
150
- cursor += 1;
151
- }
152
- if (cursor >= end) break;
153
- const char = node[cursor];
154
- if (char === '/' || char === '>') break;
155
-
156
- attrRegex.lastIndex = cursor;
157
- const match = attrRegex.exec(node);
158
- if (!match) break;
159
- attrs.set(match[1], match[3]);
160
- cursor = attrRegex.lastIndex;
161
- }
162
-
163
- return attrs;
164
- }
165
-
166
- function readXmlAttr(attrs: Map<string, string>, name: string): string | null {
167
- return attrs.get(name) ?? null;
168
- }
169
-
170
- export function parseBounds(bounds: string | null): Rect | undefined {
171
- if (!bounds) return undefined;
172
- const match = /\[(\d+),(\d+)\]\[(\d+),(\d+)\]/.exec(bounds);
173
- if (!match) return undefined;
174
- const x1 = Number(match[1]);
175
- const y1 = Number(match[2]);
176
- const x2 = Number(match[3]);
177
- const y2 = Number(match[4]);
178
- return { x: x1, y: y1, width: Math.max(0, x2 - x1), height: Math.max(0, y2 - y1) };
179
- }
180
-
181
- type AndroidNode = {
182
- type: string | null;
183
- label: string | null;
184
- value: string | null;
185
- identifier: string | null;
186
- rect?: Rect;
187
- enabled?: boolean;
188
- hittable?: boolean;
189
- depth: number;
190
- parentIndex?: number;
191
- children: AndroidNode[];
192
- };
193
-
194
- function parseUiHierarchyTree(xml: string): AndroidNode {
195
- const root: AndroidNode = {
196
- type: null,
197
- label: null,
198
- value: null,
199
- identifier: null,
200
- depth: -1,
201
- children: [],
202
- };
203
- const stack: AndroidNode[] = [root];
204
- const tokenRegex = /<node\b[^>]*>|<\/node>/g;
205
- let match = tokenRegex.exec(xml);
206
- while (match) {
207
- const token = match[0];
208
- if (token.startsWith('</node')) {
209
- if (stack.length > 1) stack.pop();
210
- match = tokenRegex.exec(xml);
211
- continue;
212
- }
213
- const attrs = readNodeAttributes(token);
214
- const rect = parseBounds(attrs.bounds);
215
- const parent = stack[stack.length - 1];
216
- const node: AndroidNode = {
217
- type: attrs.className,
218
- label: attrs.text || attrs.desc,
219
- value: attrs.text,
220
- identifier: attrs.resourceId,
221
- rect,
222
- enabled: attrs.enabled,
223
- hittable: attrs.clickable ?? attrs.focusable,
224
- depth: parent.depth + 1,
225
- parentIndex: undefined,
226
- children: [],
227
- };
228
- parent.children.push(node);
229
- if (!token.endsWith('/>')) {
230
- stack.push(node);
231
- }
232
- match = tokenRegex.exec(xml);
233
- }
234
- return root;
235
- }
236
-
237
- function shouldIncludeAndroidNode(
238
- node: AndroidNode,
239
- options: SnapshotOptions,
240
- ancestorHittable: boolean,
241
- descendantHittable: boolean,
242
- ancestorCollection: boolean,
243
- ): boolean {
244
- const type = normalizeAndroidType(node.type);
245
- const hasText = Boolean(node.label && node.label.trim().length > 0);
246
- const hasId = Boolean(node.identifier && node.identifier.trim().length > 0);
247
- const hasMeaningfulText = hasText && !isGenericAndroidId(node.label ?? '');
248
- const hasMeaningfulId = hasId && !isGenericAndroidId(node.identifier ?? '');
249
- const isStructural = isStructuralAndroidType(type);
250
- const isVisual = type === 'imageview' || type === 'imagebutton';
251
- if (options.interactiveOnly) {
252
- if (node.hittable) return true;
253
- // Keep text proxies for tappable rows while dropping structural noise.
254
- const proxyCandidate = hasMeaningfulText || hasMeaningfulId;
255
- if (!proxyCandidate) return false;
256
- if (isVisual) return false;
257
- if (isStructural && !ancestorCollection) return false;
258
- return ancestorHittable || descendantHittable || ancestorCollection;
259
- }
260
- if (options.compact) {
261
- return hasMeaningfulText || hasMeaningfulId || Boolean(node.hittable);
262
- }
263
- if (isStructural || isVisual) {
264
- if (node.hittable) return true;
265
- if (hasMeaningfulText) return true;
266
- if (hasMeaningfulId && descendantHittable) return true;
267
- return descendantHittable;
268
- }
269
- return true;
270
- }
271
-
272
- function isCollectionContainerType(type: string | null): boolean {
273
- if (!type) return false;
274
- const normalized = normalizeAndroidType(type);
275
- return (
276
- normalized.includes('recyclerview') ||
277
- normalized.includes('listview') ||
278
- normalized.includes('gridview')
279
- );
280
- }
281
-
282
- function normalizeAndroidType(type: string | null): string {
283
- if (!type) return '';
284
- return type.toLowerCase();
285
- }
286
-
287
- function isStructuralAndroidType(type: string): boolean {
288
- const short = type.split('.').pop() ?? type;
289
- return short.includes('layout') || short === 'viewgroup' || short === 'view';
290
- }
291
-
292
- function isGenericAndroidId(value: string): boolean {
293
- const trimmed = value.trim();
294
- if (!trimmed) return false;
295
- return /^[\w.]+:id\/[\w.-]+$/i.test(trimmed);
296
- }
297
-
298
- function findScopeNode(root: AndroidNode, scope: string): AndroidNode | null {
299
- const query = scope.toLowerCase();
300
- const stack: AndroidNode[] = [...root.children];
301
- while (stack.length > 0) {
302
- const node = stack.shift() as AndroidNode;
303
- const label = node.label?.toLowerCase() ?? '';
304
- const value = node.value?.toLowerCase() ?? '';
305
- const identifier = node.identifier?.toLowerCase() ?? '';
306
- if (label.includes(query) || value.includes(query) || identifier.includes(query)) {
307
- return node;
308
- }
309
- stack.push(...node.children);
310
- }
311
- return null;
312
- }
@@ -1,128 +0,0 @@
1
- import { asAppError } from '../utils/errors.ts';
2
-
3
- export type BootFailureReason =
4
- | 'IOS_BOOT_TIMEOUT'
5
- | 'IOS_RUNNER_CONNECT_TIMEOUT'
6
- | 'IOS_TOOL_MISSING'
7
- | 'ANDROID_BOOT_TIMEOUT'
8
- | 'ADB_TRANSPORT_UNAVAILABLE'
9
- | 'CI_RESOURCE_STARVATION_SUSPECTED'
10
- | 'BOOT_COMMAND_FAILED'
11
- | 'UNKNOWN';
12
-
13
- type BootDiagnosticContext = {
14
- platform?: 'ios' | 'android';
15
- phase?: 'boot' | 'connect' | 'transport';
16
- };
17
-
18
- export function classifyBootFailure(input: {
19
- error?: unknown;
20
- message?: string;
21
- stdout?: string;
22
- stderr?: string;
23
- context?: BootDiagnosticContext;
24
- }): BootFailureReason {
25
- const appErr = input.error ? asAppError(input.error) : null;
26
- const platform = input.context?.platform;
27
- const phase = input.context?.phase;
28
- if (appErr?.code === 'TOOL_MISSING') {
29
- return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'IOS_TOOL_MISSING';
30
- }
31
- const details = (appErr?.details ?? {}) as Record<string, unknown>;
32
- const detailMessage = typeof details.message === 'string' ? details.message : undefined;
33
- const detailStdout = typeof details.stdout === 'string' ? details.stdout : undefined;
34
- const detailStderr = typeof details.stderr === 'string' ? details.stderr : undefined;
35
- const nestedBoot = details.boot && typeof details.boot === 'object'
36
- ? (details.boot as Record<string, unknown>)
37
- : null;
38
- const nestedBootstatus = details.bootstatus && typeof details.bootstatus === 'object'
39
- ? (details.bootstatus as Record<string, unknown>)
40
- : null;
41
-
42
- const haystack = [
43
- input.message,
44
- appErr?.message,
45
- input.stdout,
46
- input.stderr,
47
- detailMessage,
48
- detailStdout,
49
- detailStderr,
50
- typeof nestedBoot?.stdout === 'string' ? nestedBoot.stdout : undefined,
51
- typeof nestedBoot?.stderr === 'string' ? nestedBoot.stderr : undefined,
52
- typeof nestedBootstatus?.stdout === 'string' ? nestedBootstatus.stdout : undefined,
53
- typeof nestedBootstatus?.stderr === 'string' ? nestedBootstatus.stderr : undefined,
54
- ]
55
- .filter(Boolean)
56
- .join('\n')
57
- .toLowerCase();
58
-
59
- if (
60
- platform === 'ios' &&
61
- (
62
- haystack.includes('runner did not accept connection') ||
63
- (phase === 'connect' &&
64
- (
65
- haystack.includes('timed out') ||
66
- haystack.includes('timeout') ||
67
- haystack.includes('econnrefused') ||
68
- haystack.includes('connection refused') ||
69
- haystack.includes('fetch failed') ||
70
- haystack.includes('socket hang up')
71
- ))
72
- )
73
- ) {
74
- return 'IOS_RUNNER_CONNECT_TIMEOUT';
75
- }
76
- if (platform === 'ios' && phase === 'boot' && (haystack.includes('timed out') || haystack.includes('timeout'))) {
77
- return 'IOS_BOOT_TIMEOUT';
78
- }
79
- if (platform === 'android' && phase === 'boot' && (haystack.includes('timed out') || haystack.includes('timeout'))) {
80
- return 'ANDROID_BOOT_TIMEOUT';
81
- }
82
- if (
83
- haystack.includes('resource temporarily unavailable') ||
84
- haystack.includes('killed: 9') ||
85
- haystack.includes('cannot allocate memory') ||
86
- haystack.includes('system is low on memory')
87
- ) {
88
- return 'CI_RESOURCE_STARVATION_SUSPECTED';
89
- }
90
- if (
91
- platform === 'android' &&
92
- (
93
- haystack.includes('device not found') ||
94
- haystack.includes('no devices') ||
95
- haystack.includes('device offline') ||
96
- haystack.includes('offline') ||
97
- haystack.includes('unauthorized') ||
98
- haystack.includes('not authorized') ||
99
- haystack.includes('unable to locate device') ||
100
- haystack.includes('invalid device')
101
- )
102
- ) {
103
- return 'ADB_TRANSPORT_UNAVAILABLE';
104
- }
105
- if (appErr?.code === 'COMMAND_FAILED' || haystack.length > 0) return 'BOOT_COMMAND_FAILED';
106
- return 'UNKNOWN';
107
- }
108
-
109
- export function bootFailureHint(reason: BootFailureReason): string {
110
- switch (reason) {
111
- case 'IOS_BOOT_TIMEOUT':
112
- return 'Retry simulator boot and inspect simctl bootstatus logs; in CI consider increasing AGENT_DEVICE_IOS_BOOT_TIMEOUT_MS.';
113
- case 'IOS_RUNNER_CONNECT_TIMEOUT':
114
- return 'Retry runner startup, inspect xcodebuild logs, and verify simulator responsiveness before command execution.';
115
- case 'ANDROID_BOOT_TIMEOUT':
116
- return 'Retry emulator startup and verify sys.boot_completed reaches 1; consider increasing startup budget in CI.';
117
- case 'ADB_TRANSPORT_UNAVAILABLE':
118
- return 'Check adb server/device transport (adb devices -l), restart adb, and ensure the target device is online and authorized.';
119
- case 'CI_RESOURCE_STARVATION_SUSPECTED':
120
- return 'CI machine may be resource constrained; reduce parallel jobs or use a larger runner.';
121
- case 'IOS_TOOL_MISSING':
122
- return 'Xcode command-line tools are missing or not in PATH; run xcode-select --install and verify xcrun works.';
123
- case 'BOOT_COMMAND_FAILED':
124
- return 'Inspect command stderr/stdout for the failing boot phase and retry after environment validation.';
125
- default:
126
- return 'Retry once and inspect verbose logs for the failing phase.';
127
- }
128
- }
@@ -1,24 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { openIosApp } from '../index.ts';
4
- import type { DeviceInfo } from '../../../utils/device.ts';
5
- import { AppError } from '../../../utils/errors.ts';
6
-
7
- test('openIosApp rejects deep links on iOS physical devices', async () => {
8
- const device: DeviceInfo = {
9
- platform: 'ios',
10
- id: 'ios-device-1',
11
- name: 'iPhone Device',
12
- kind: 'device',
13
- booted: true,
14
- };
15
-
16
- await assert.rejects(
17
- () => openIosApp(device, 'https://example.com/path'),
18
- (error: unknown) => {
19
- assert.equal(error instanceof AppError, true);
20
- assert.equal((error as AppError).code, 'UNSUPPORTED_OPERATION');
21
- return true;
22
- },
23
- );
24
- });
@@ -1,113 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import type { DeviceInfo } from '../../../utils/device.ts';
4
- import {
5
- assertSafeDerivedCleanup,
6
- resolveRunnerBuildDestination,
7
- resolveRunnerDestination,
8
- resolveRunnerMaxConcurrentDestinationsFlag,
9
- resolveRunnerSigningBuildSettings,
10
- } from '../runner-client.ts';
11
-
12
- const iosSimulator: DeviceInfo = {
13
- platform: 'ios',
14
- id: 'sim-1',
15
- name: 'iPhone Simulator',
16
- kind: 'simulator',
17
- booted: true,
18
- };
19
-
20
- const iosDevice: DeviceInfo = {
21
- platform: 'ios',
22
- id: '00008110-000E12341234002E',
23
- name: 'iPhone',
24
- kind: 'device',
25
- booted: true,
26
- };
27
-
28
- test('resolveRunnerDestination uses simulator destination for simulators', () => {
29
- assert.equal(resolveRunnerDestination(iosSimulator), 'platform=iOS Simulator,id=sim-1');
30
- });
31
-
32
- test('resolveRunnerDestination uses device destination for physical devices', () => {
33
- assert.equal(
34
- resolveRunnerDestination(iosDevice),
35
- 'platform=iOS,id=00008110-000E12341234002E',
36
- );
37
- });
38
-
39
- test('resolveRunnerBuildDestination uses generic iOS destination for physical devices', () => {
40
- assert.equal(resolveRunnerBuildDestination(iosDevice), 'generic/platform=iOS');
41
- });
42
-
43
- test('resolveRunnerMaxConcurrentDestinationsFlag uses simulator flag for simulators', () => {
44
- assert.equal(
45
- resolveRunnerMaxConcurrentDestinationsFlag(iosSimulator),
46
- '-maximum-concurrent-test-simulator-destinations',
47
- );
48
- });
49
-
50
- test('resolveRunnerMaxConcurrentDestinationsFlag uses device flag for physical devices', () => {
51
- assert.equal(
52
- resolveRunnerMaxConcurrentDestinationsFlag(iosDevice),
53
- '-maximum-concurrent-test-device-destinations',
54
- );
55
- });
56
-
57
- test('resolveRunnerSigningBuildSettings returns empty args without env overrides', () => {
58
- assert.deepEqual(resolveRunnerSigningBuildSettings({}), []);
59
- });
60
-
61
- test('resolveRunnerSigningBuildSettings enables automatic signing for device builds without forcing identity', () => {
62
- assert.deepEqual(resolveRunnerSigningBuildSettings({}, true), [
63
- 'CODE_SIGN_STYLE=Automatic',
64
- ]);
65
- });
66
-
67
- test('resolveRunnerSigningBuildSettings ignores device signing overrides for simulator builds', () => {
68
- assert.deepEqual(resolveRunnerSigningBuildSettings({
69
- AGENT_DEVICE_IOS_TEAM_ID: 'ABCDE12345',
70
- AGENT_DEVICE_IOS_SIGNING_IDENTITY: 'Apple Development',
71
- AGENT_DEVICE_IOS_PROVISIONING_PROFILE: 'My Profile',
72
- }, false), []);
73
- });
74
-
75
- test('resolveRunnerSigningBuildSettings applies optional overrides when provided', () => {
76
- const settings = resolveRunnerSigningBuildSettings({
77
- AGENT_DEVICE_IOS_TEAM_ID: 'ABCDE12345',
78
- AGENT_DEVICE_IOS_SIGNING_IDENTITY: 'Apple Development',
79
- AGENT_DEVICE_IOS_PROVISIONING_PROFILE: 'My Profile',
80
- }, true);
81
- assert.deepEqual(settings, [
82
- 'CODE_SIGN_STYLE=Automatic',
83
- 'DEVELOPMENT_TEAM=ABCDE12345',
84
- 'CODE_SIGN_IDENTITY=Apple Development',
85
- 'PROVISIONING_PROFILE_SPECIFIER=My Profile',
86
- ]);
87
- });
88
-
89
- test('assertSafeDerivedCleanup allows cleaning when no override is set', () => {
90
- assert.doesNotThrow(() => {
91
- assertSafeDerivedCleanup('/tmp/derived', {});
92
- });
93
- });
94
-
95
- test('assertSafeDerivedCleanup rejects cleaning override path by default', () => {
96
- assert.throws(
97
- () => {
98
- assertSafeDerivedCleanup('/tmp/custom', {
99
- AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: '/tmp/custom',
100
- });
101
- },
102
- /Refusing to clean AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH automatically/,
103
- );
104
- });
105
-
106
- test('assertSafeDerivedCleanup allows cleaning override path with explicit opt-in', () => {
107
- assert.doesNotThrow(() => {
108
- assertSafeDerivedCleanup('/tmp/custom', {
109
- AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: '/tmp/custom',
110
- AGENT_DEVICE_IOS_ALLOW_OVERRIDE_DERIVED_CLEAN: '1',
111
- });
112
- });
113
- });