From 15343c425c43e88b7d5616ef4d8af3cdd10adca3 Mon Sep 17 00:00:00 2001 From: samwize <68338+samwize@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:24:45 +0800 Subject: [PATCH 1/2] fix(ios): respect devicectl minimum timeout for device probes - Clamp the CLI timeout to five seconds without extending the host deadline. - Cover short-budget tunnel lookups and timeout boundaries with regression tests. --- .../physical-device-coredevice.test.ts | 40 +++++++++++++++++++ .../src/core/physical-device-coredevice.ts | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts index 6b9f1ab3d2..5ca51fd7d3 100644 --- a/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts +++ b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts @@ -486,6 +486,46 @@ test('the CoreDevice backend publishes the device report', async () => { assert.equal(readiness.developerDiskImage, 'available'); }); +test.each([ + [250, '5'], + [1000, '5'], + [4000, '5'], + [4999, '5'], + [5000, '5'], + [5001, '6'], + [10000, '10'], +])( + 'CoreDevice tunnel lookup keeps a %i ms host budget with CLI timeout %s', + async (budget, cliTimeout) => { + let observedCliTimeout: string | undefined; + let observedHostTimeout: number | undefined; + const tunnel = await withAppleToolProvider( + createLocalAppleToolProvider({ + runCommand: async (_cmd, args, options) => { + observedCliTimeout = args[args.indexOf('--timeout') + 1]; + observedHostTimeout = options?.timeoutMs; + if (Number(observedCliTimeout) < 5) { + return { + exitCode: 64, + stdout: '', + stderr: "Error: Please specify a 'timeout' value between 5 and 9223372036854775807", + }; + } + const outputPath = jsonOutputPath(args); + if (outputPath) fs.writeFileSync(outputPath, DEVICE_INFO_DETAILS_TEXT); + return { exitCode: 0, stdout: '', stderr: '' }; + }, + }), + async () => + await resolveIosPhysicalDeviceControl(IOS_DEVICE).resolveTunnel(IOS_DEVICE, budget), + ); + + assert.deepEqual(tunnel, { tunnelIp: 'fd00:0000:0000::1' }); + assert.equal(observedCliTimeout, cliTimeout); + assert.equal(observedHostTimeout, budget); + }, +); + /** * A `deviceProperties` payload together with the context it was read in. Both default to the state a * developer disk image answer requires — tunnel up, phone booted — because #2683 treats that context diff --git a/packages/platform-apple/src/core/physical-device-coredevice.ts b/packages/platform-apple/src/core/physical-device-coredevice.ts index 7a78065003..ab69daf451 100644 --- a/packages/platform-apple/src/core/physical-device-coredevice.ts +++ b/packages/platform-apple/src/core/physical-device-coredevice.ts @@ -177,7 +177,7 @@ async function runCoreDeviceDetails( commandTimeoutBufferMs = 0, signal?: AbortSignal, ): Promise { - const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000)); + const timeoutSeconds = Math.max(5, Math.ceil(timeoutMs / 1000)); const outcome = await runIosDevicectlJsonRequest({ jsonPrefix: 'agent-device-coredevice-info', args: [ From c2ed74a9ff3bf133d2872b7401bc854c2a7d8003 Mon Sep 17 00:00:00 2001 From: samwize <68338+samwize@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:17:38 +0800 Subject: [PATCH 2/2] fix: share the devicectl minimum timeout contract - Reuse one timeout floor for device details and hinge-angle probes. - Clarify host deadlines and retain the focused boundary regression cases. --- .../src/core/__tests__/physical-device-coredevice.test.ts | 5 ++--- packages/platform-apple/src/core/config.ts | 8 +++++--- .../platform-apple/src/core/physical-device-coredevice.ts | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts index 5ca51fd7d3..9a44a95e34 100644 --- a/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts +++ b/packages/platform-apple/src/core/__tests__/physical-device-coredevice.test.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; +import { IOS_DEVICECTL_MIN_TIMEOUT_SECONDS } from '../config.ts'; import { launchCoreDeviceApp, parseIosDeviceDetailsPayload, @@ -488,8 +489,6 @@ test('the CoreDevice backend publishes the device report', async () => { test.each([ [250, '5'], - [1000, '5'], - [4000, '5'], [4999, '5'], [5000, '5'], [5001, '6'], @@ -504,7 +503,7 @@ test.each([ runCommand: async (_cmd, args, options) => { observedCliTimeout = args[args.indexOf('--timeout') + 1]; observedHostTimeout = options?.timeoutMs; - if (Number(observedCliTimeout) < 5) { + if (Number(observedCliTimeout) < IOS_DEVICECTL_MIN_TIMEOUT_SECONDS) { return { exitCode: 64, stdout: '', diff --git a/packages/platform-apple/src/core/config.ts b/packages/platform-apple/src/core/config.ts index 8f2ebbe96f..19529bae06 100644 --- a/packages/platform-apple/src/core/config.ts +++ b/packages/platform-apple/src/core/config.ts @@ -5,6 +5,8 @@ export const IOS_SIMCTL_LIST_TIMEOUT_MS = 60_000; export const IOS_APP_LAUNCH_TIMEOUT_MS = 60_000; export const IOS_DEVICECTL_TIMEOUT_MS = 20_000; +/** The smallest `--timeout` in seconds devicectl accepts; smaller values exit 64 before querying. */ +export const IOS_DEVICECTL_MIN_TIMEOUT_SECONDS = 5; export const IOS_DEVICE_INSTALL_TIMEOUT_MS = 120_000; @@ -20,10 +22,10 @@ export const IOS_SIMULATOR_SCREENSHOT_TIMEOUT_MS = 20_000; // request-level daemon reset. Measured probe cost is ~0.2s. export const IOS_APPLE_DISPLAY_PROBE_TIMEOUT_MS = 5_000; /** - * The smallest `--timeout` devicectl accepts. The hinge-angle stream never ends on its own, so - * one read costs exactly this long and the exec deadline below only guards a wedged CoreDevice. + * The hinge-angle stream never ends on its own, so one read costs the devicectl minimum + * and the exec deadline below only guards a wedged CoreDevice. */ -export const IOS_HINGE_ANGLE_STREAM_SECONDS = 5; +export const IOS_HINGE_ANGLE_STREAM_SECONDS = IOS_DEVICECTL_MIN_TIMEOUT_SECONDS; export const IOS_HINGE_ANGLE_TIMEOUT_MS = 20_000; /** How many hinge reads a dispatched hinge request gets to reach its pose before the pose is refused. */ export const IOS_FOLD_POSE_SETTLE_ATTEMPTS = 4; diff --git a/packages/platform-apple/src/core/physical-device-coredevice.ts b/packages/platform-apple/src/core/physical-device-coredevice.ts index ab69daf451..1201c56ec0 100644 --- a/packages/platform-apple/src/core/physical-device-coredevice.ts +++ b/packages/platform-apple/src/core/physical-device-coredevice.ts @@ -1,6 +1,7 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { execFailureDetails, type ExecResult } from '@agent-device/host-kit/command'; +import { IOS_DEVICECTL_MIN_TIMEOUT_SECONDS } from './config.ts'; import { IOS_DEVICE_DEVELOPER_DISK_IMAGE_HINT, IOS_DEVICE_DEVELOPER_MODE_OFF_HINT, @@ -177,7 +178,8 @@ async function runCoreDeviceDetails( commandTimeoutBufferMs = 0, signal?: AbortSignal, ): Promise { - const timeoutSeconds = Math.max(5, Math.ceil(timeoutMs / 1000)); + // devicectl rejects sub-minimum values; shorter budgets use the host deadline alone. + const timeoutSeconds = Math.max(IOS_DEVICECTL_MIN_TIMEOUT_SECONDS, Math.ceil(timeoutMs / 1000)); const outcome = await runIosDevicectlJsonRequest({ jsonPrefix: 'agent-device-coredevice-info', args: [