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,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
|
-
}
|