Skip to content

Commit 85c5d3d

Browse files
thymikeecursoragent
andcommitted
Fix duplicate find matching, add IOS_TOOL_MISSING reason, DRY retry log env check
- Remove redundant findNodeByLocator call in find handler; reuse bestMatches from the ambiguity check instead of scanning nodes twice. - Add IOS_TOOL_MISSING boot failure reason so xcrun/simctl absence gets a specific diagnostic hint instead of generic BOOT_COMMAND_FAILED. - Extract isEnvTruthy helper into retry.ts and replace duplicated RETRY_LOGS_ENABLED parsing in ios/index.ts and android/devices.ts. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8332238 commit 85c5d3d

6 files changed

Lines changed: 24 additions & 13 deletions

File tree

src/daemon/handlers/find.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts';
2-
import { findBestMatchesByLocator, findNodeByLocator, type FindLocator } from '../../utils/finders.ts';
2+
import { findBestMatchesByLocator, type FindLocator } from '../../utils/finders.ts';
33
import { attachRefs, centerOfRect, type RawSnapshotNode, type SnapshotState } from '../../utils/snapshot.ts';
44
import { AppError } from '../../utils/errors.ts';
55
import type { DaemonRequest, DaemonResponse } from '../types.ts';
@@ -97,7 +97,7 @@ export async function handleFindCommands(params: {
9797
const start = Date.now();
9898
while (Date.now() - start < timeout) {
9999
const { nodes } = await fetchNodes();
100-
const match = findNodeByLocator(nodes, locator, query, { requireRect: false });
100+
const match = findBestMatchesByLocator(nodes, locator, query, { requireRect: false }).matches[0];
101101
if (match) {
102102
if (session) {
103103
sessionStore.recordAction(session, {
@@ -134,7 +134,7 @@ export async function handleFindCommands(params: {
134134
},
135135
};
136136
}
137-
const node = findNodeByLocator(nodes, locator, query, { requireRect: requiresRect });
137+
const node = bestMatches.matches[0] ?? null;
138138
if (!node) {
139139
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'find did not match any element' } };
140140
}

src/platforms/__tests__/boot-diagnostics.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@ test('classifyBootFailure maps adb offline errors', () => {
1919
assert.equal(reason, 'ADB_TRANSPORT_UNAVAILABLE');
2020
});
2121

22-
test('classifyBootFailure maps tool missing from AppError code', () => {
22+
test('classifyBootFailure maps tool missing from AppError code (android)', () => {
2323
const reason = classifyBootFailure({
2424
error: new AppError('TOOL_MISSING', 'adb not found in PATH'),
2525
context: { platform: 'android', phase: 'transport' },
2626
});
2727
assert.equal(reason, 'ADB_TRANSPORT_UNAVAILABLE');
2828
});
2929

30+
test('classifyBootFailure maps tool missing from AppError code (ios)', () => {
31+
const reason = classifyBootFailure({
32+
error: new AppError('TOOL_MISSING', 'xcrun not found in PATH'),
33+
context: { platform: 'ios', phase: 'boot' },
34+
});
35+
assert.equal(reason, 'IOS_TOOL_MISSING');
36+
});
37+
3038
test('classifyBootFailure reads stderr from AppError details', () => {
3139
const reason = classifyBootFailure({
3240
error: new AppError('COMMAND_FAILED', 'adb failed', {

src/platforms/android/devices.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import { runCmd, whichCmd } from '../../utils/exec.ts';
22
import type { ExecResult } from '../../utils/exec.ts';
33
import { AppError, asAppError } from '../../utils/errors.ts';
44
import type { DeviceInfo } from '../../utils/device.ts';
5-
import { Deadline, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts';
5+
import { Deadline, isEnvTruthy, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts';
66
import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts';
77

88
const EMULATOR_SERIAL_PREFIX = 'emulator-';
99
const ANDROID_BOOT_POLL_MS = 1000;
10-
const RETRY_LOGS_ENABLED = ['1', 'true', 'yes', 'on'].includes(
11-
(process.env.AGENT_DEVICE_RETRY_LOGS ?? '').toLowerCase(),
12-
);
10+
const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS);
1311

1412
function adbArgs(serial: string, args: string[]): string[] {
1513
return ['-s', serial, ...args];

src/platforms/boot-diagnostics.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { asAppError } from '../utils/errors.ts';
33
export type BootFailureReason =
44
| 'IOS_BOOT_TIMEOUT'
55
| 'IOS_RUNNER_CONNECT_TIMEOUT'
6+
| 'IOS_TOOL_MISSING'
67
| 'ANDROID_BOOT_TIMEOUT'
78
| 'ADB_TRANSPORT_UNAVAILABLE'
89
| 'CI_RESOURCE_STARVATION_SUSPECTED'
@@ -25,7 +26,7 @@ export function classifyBootFailure(input: {
2526
const platform = input.context?.platform;
2627
const phase = input.context?.phase;
2728
if (appErr?.code === 'TOOL_MISSING') {
28-
return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'BOOT_COMMAND_FAILED';
29+
return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'IOS_TOOL_MISSING';
2930
}
3031
const details = (appErr?.details ?? {}) as Record<string, unknown>;
3132
const detailMessage = typeof details.message === 'string' ? details.message : undefined;
@@ -117,6 +118,8 @@ export function bootFailureHint(reason: BootFailureReason): string {
117118
return 'Check adb server/device transport (adb devices -l), restart adb, and ensure the target device is online and authorized.';
118119
case 'CI_RESOURCE_STARVATION_SUSPECTED':
119120
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.';
120123
case 'BOOT_COMMAND_FAILED':
121124
return 'Inspect command stderr/stdout for the failing boot phase and retry after environment validation.';
122125
default:

src/platforms/ios/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { runCmd } from '../../utils/exec.ts';
22
import type { ExecResult } from '../../utils/exec.ts';
33
import { AppError } from '../../utils/errors.ts';
44
import type { DeviceInfo } from '../../utils/device.ts';
5-
import { Deadline, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts';
5+
import { Deadline, isEnvTruthy, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts';
66
import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts';
77

88
const ALIASES: Record<string, string> = {
@@ -14,9 +14,7 @@ const IOS_BOOT_TIMEOUT_MS = resolveTimeoutMs(
1414
TIMEOUT_PROFILES.ios_boot.totalMs,
1515
5_000,
1616
);
17-
const RETRY_LOGS_ENABLED = ['1', 'true', 'yes', 'on'].includes(
18-
(process.env.AGENT_DEVICE_RETRY_LOGS ?? '').toLowerCase(),
19-
);
17+
const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS);
2018

2119
export async function resolveIosApp(device: DeviceInfo, app: string): Promise<string> {
2220
const trimmed = app.trim();

src/utils/retry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export type RetryTelemetryEvent = {
3939
reason?: string;
4040
};
4141

42+
export function isEnvTruthy(value: string | undefined): boolean {
43+
return ['1', 'true', 'yes', 'on'].includes((value ?? '').toLowerCase());
44+
}
45+
4246
export const TIMEOUT_PROFILES: Record<string, TimeoutProfile> = {
4347
ios_boot: { startupMs: 120_000, operationMs: 20_000, totalMs: 120_000 },
4448
ios_runner_connect: { startupMs: 120_000, operationMs: 15_000, totalMs: 120_000 },

0 commit comments

Comments
 (0)