diff --git a/src/daemon/handlers/find.ts b/src/daemon/handlers/find.ts index c55698bfa..b2f6461a8 100644 --- a/src/daemon/handlers/find.ts +++ b/src/daemon/handlers/find.ts @@ -1,5 +1,5 @@ import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts'; -import { findBestMatchesByLocator, findNodeByLocator, type FindLocator } from '../../utils/finders.ts'; +import { findBestMatchesByLocator, type FindLocator } from '../../utils/finders.ts'; import { attachRefs, centerOfRect, type RawSnapshotNode, type SnapshotState } from '../../utils/snapshot.ts'; import { AppError } from '../../utils/errors.ts'; import type { DaemonRequest, DaemonResponse } from '../types.ts'; @@ -97,7 +97,7 @@ export async function handleFindCommands(params: { const start = Date.now(); while (Date.now() - start < timeout) { const { nodes } = await fetchNodes(); - const match = findNodeByLocator(nodes, locator, query, { requireRect: false }); + const match = findBestMatchesByLocator(nodes, locator, query, { requireRect: false }).matches[0]; if (match) { if (session) { sessionStore.recordAction(session, { @@ -134,7 +134,7 @@ export async function handleFindCommands(params: { }, }; } - const node = findNodeByLocator(nodes, locator, query, { requireRect: requiresRect }); + const node = bestMatches.matches[0] ?? null; if (!node) { return { ok: false, error: { code: 'COMMAND_FAILED', message: 'find did not match any element' } }; } diff --git a/src/platforms/__tests__/boot-diagnostics.test.ts b/src/platforms/__tests__/boot-diagnostics.test.ts index 518668d55..97a931678 100644 --- a/src/platforms/__tests__/boot-diagnostics.test.ts +++ b/src/platforms/__tests__/boot-diagnostics.test.ts @@ -19,7 +19,7 @@ test('classifyBootFailure maps adb offline errors', () => { assert.equal(reason, 'ADB_TRANSPORT_UNAVAILABLE'); }); -test('classifyBootFailure maps tool missing from AppError code', () => { +test('classifyBootFailure maps tool missing from AppError code (android)', () => { const reason = classifyBootFailure({ error: new AppError('TOOL_MISSING', 'adb not found in PATH'), context: { platform: 'android', phase: 'transport' }, @@ -27,6 +27,14 @@ test('classifyBootFailure maps tool missing from AppError code', () => { assert.equal(reason, 'ADB_TRANSPORT_UNAVAILABLE'); }); +test('classifyBootFailure maps tool missing from AppError code (ios)', () => { + const reason = classifyBootFailure({ + error: new AppError('TOOL_MISSING', 'xcrun not found in PATH'), + context: { platform: 'ios', phase: 'boot' }, + }); + assert.equal(reason, 'IOS_TOOL_MISSING'); +}); + test('classifyBootFailure reads stderr from AppError details', () => { const reason = classifyBootFailure({ error: new AppError('COMMAND_FAILED', 'adb failed', { diff --git a/src/platforms/android/devices.ts b/src/platforms/android/devices.ts index 0e8bd1a9d..7f97e59a9 100644 --- a/src/platforms/android/devices.ts +++ b/src/platforms/android/devices.ts @@ -2,14 +2,12 @@ import { runCmd, whichCmd } from '../../utils/exec.ts'; import type { ExecResult } from '../../utils/exec.ts'; import { AppError, asAppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; -import { Deadline, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; +import { Deadline, isEnvTruthy, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts'; const EMULATOR_SERIAL_PREFIX = 'emulator-'; const ANDROID_BOOT_POLL_MS = 1000; -const RETRY_LOGS_ENABLED = ['1', 'true', 'yes', 'on'].includes( - (process.env.AGENT_DEVICE_RETRY_LOGS ?? '').toLowerCase(), -); +const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS); function adbArgs(serial: string, args: string[]): string[] { return ['-s', serial, ...args]; diff --git a/src/platforms/boot-diagnostics.ts b/src/platforms/boot-diagnostics.ts index c18140c5e..6fc116886 100644 --- a/src/platforms/boot-diagnostics.ts +++ b/src/platforms/boot-diagnostics.ts @@ -3,6 +3,7 @@ import { asAppError } from '../utils/errors.ts'; export type BootFailureReason = | 'IOS_BOOT_TIMEOUT' | 'IOS_RUNNER_CONNECT_TIMEOUT' + | 'IOS_TOOL_MISSING' | 'ANDROID_BOOT_TIMEOUT' | 'ADB_TRANSPORT_UNAVAILABLE' | 'CI_RESOURCE_STARVATION_SUSPECTED' @@ -25,7 +26,7 @@ export function classifyBootFailure(input: { const platform = input.context?.platform; const phase = input.context?.phase; if (appErr?.code === 'TOOL_MISSING') { - return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'BOOT_COMMAND_FAILED'; + return platform === 'android' ? 'ADB_TRANSPORT_UNAVAILABLE' : 'IOS_TOOL_MISSING'; } const details = (appErr?.details ?? {}) as Record; const detailMessage = typeof details.message === 'string' ? details.message : undefined; @@ -117,6 +118,8 @@ export function bootFailureHint(reason: BootFailureReason): string { return 'Check adb server/device transport (adb devices -l), restart adb, and ensure the target device is online and authorized.'; case 'CI_RESOURCE_STARVATION_SUSPECTED': return 'CI machine may be resource constrained; reduce parallel jobs or use a larger runner.'; + case 'IOS_TOOL_MISSING': + return 'Xcode command-line tools are missing or not in PATH; run xcode-select --install and verify xcrun works.'; case 'BOOT_COMMAND_FAILED': return 'Inspect command stderr/stdout for the failing boot phase and retry after environment validation.'; default: diff --git a/src/platforms/ios/index.ts b/src/platforms/ios/index.ts index a43c9c8f4..9dbf8fd13 100644 --- a/src/platforms/ios/index.ts +++ b/src/platforms/ios/index.ts @@ -2,7 +2,7 @@ import { runCmd } from '../../utils/exec.ts'; import type { ExecResult } from '../../utils/exec.ts'; import { AppError } from '../../utils/errors.ts'; import type { DeviceInfo } from '../../utils/device.ts'; -import { Deadline, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; +import { Deadline, isEnvTruthy, retryWithPolicy, TIMEOUT_PROFILES, type RetryTelemetryEvent } from '../../utils/retry.ts'; import { bootFailureHint, classifyBootFailure } from '../boot-diagnostics.ts'; const ALIASES: Record = { @@ -14,9 +14,7 @@ const IOS_BOOT_TIMEOUT_MS = resolveTimeoutMs( TIMEOUT_PROFILES.ios_boot.totalMs, 5_000, ); -const RETRY_LOGS_ENABLED = ['1', 'true', 'yes', 'on'].includes( - (process.env.AGENT_DEVICE_RETRY_LOGS ?? '').toLowerCase(), -); +const RETRY_LOGS_ENABLED = isEnvTruthy(process.env.AGENT_DEVICE_RETRY_LOGS); export async function resolveIosApp(device: DeviceInfo, app: string): Promise { const trimmed = app.trim(); diff --git a/src/utils/retry.ts b/src/utils/retry.ts index fb8262485..d07b61cc1 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -39,6 +39,10 @@ export type RetryTelemetryEvent = { reason?: string; }; +export function isEnvTruthy(value: string | undefined): boolean { + return ['1', 'true', 'yes', 'on'].includes((value ?? '').toLowerCase()); +} + export const TIMEOUT_PROFILES: Record = { ios_boot: { startupMs: 120_000, operationMs: 20_000, totalMs: 120_000 }, ios_runner_connect: { startupMs: 120_000, operationMs: 15_000, totalMs: 120_000 },