Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/daemon/handlers/find.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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' } };
}
Expand Down
10 changes: 9 additions & 1 deletion src/platforms/__tests__/boot-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ 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' },
});
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', {
Expand Down
6 changes: 2 additions & 4 deletions src/platforms/android/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
5 changes: 4 additions & 1 deletion src/platforms/boot-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string, unknown>;
const detailMessage = typeof details.message === 'string' ? details.message : undefined;
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 2 additions & 4 deletions src/platforms/ios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
Expand All @@ -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<string> {
const trimmed = app.trim();
Expand Down
4 changes: 4 additions & 0 deletions src/utils/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TimeoutProfile> = {
ios_boot: { startupMs: 120_000, operationMs: 20_000, totalMs: 120_000 },
ios_runner_connect: { startupMs: 120_000, operationMs: 15_000, totalMs: 120_000 },
Expand Down
Loading