From 08be0f5d60acf2cf9dd63fe7150d2d0813e4c5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 1 Aug 2026 11:22:46 +0200 Subject: [PATCH] refactor(ios): drop the transport seam usbmux-first made dead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The physical-device control exposed resolveRunnerTransport returning either a network tunnel or usbmux, from before the route resolver decided transports. Since #1517 the resolver returns a usbmux route for XCTest devices and for any attached CoreDevice one, and only reaches the control after usbmux has reported the device unattached — so the control is asked exclusively for a tunnel. That left the usbmux arm with no live producer or consumer: the branch handling it in the resolver was unreachable, and the XCTest implementation returning it was called only by a test. Collapse the union to the one shape that is resolved, rename the seam to say what it does, delete the unreachable branch, and let XCTest reject a tunnel lookup the way it already rejects app inventory and process lookup — it has no CoreDevice tunnel, which is why the resolver never asks it for one. --- .../__tests__/physical-device-control.test.ts | 15 ++++++--- .../apple/core/physical-device-control.ts | 31 +++++++++++++------ .../apple/core/runner/runner-command-route.ts | 6 +--- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/platforms/apple/core/__tests__/physical-device-control.test.ts b/src/platforms/apple/core/__tests__/physical-device-control.test.ts index 1e401fce76..9461271848 100644 --- a/src/platforms/apple/core/__tests__/physical-device-control.test.ts +++ b/src/platforms/apple/core/__tests__/physical-device-control.test.ts @@ -5,6 +5,7 @@ import path from 'node:path'; import { test } from 'vitest'; import { IOS_DEVICE as SHARED_IOS_DEVICE } from '../../../../__tests__/test-utils/index.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; +import type { AppError } from '@agent-device/kernel/errors'; import { createAppleInteractor } from '../../interactor.ts'; import { installIosApp } from '../app-install.ts'; import { closeIosApp, openIosApp } from '../app-launch.ts'; @@ -47,11 +48,15 @@ test('XCTest readiness uses xcdevice instead of devicectl', async () => { args: ['xcdevice', 'wait', '--both', '--timeout=15', XCTEST_IOS_DEVICE.id], }, ]); - assert.deepEqual( - await resolveIosPhysicalDeviceControl(XCTEST_IOS_DEVICE).resolveRunnerTransport( - XCTEST_IOS_DEVICE, - ), - { kind: 'usbmux' }, + // An XCTest-backed device has no CoreDevice tunnel, and the route resolver + // never asks it for one: it returns a usbmux route before reaching this. + await assert.rejects( + async () => + await resolveIosPhysicalDeviceControl(XCTEST_IOS_DEVICE).resolveTunnel(XCTEST_IOS_DEVICE), + (error: unknown) => { + assert.equal((error as AppError).code, 'UNSUPPORTED_OPERATION'); + return true; + }, ); }); diff --git a/src/platforms/apple/core/physical-device-control.ts b/src/platforms/apple/core/physical-device-control.ts index 732f595e91..673e12349e 100644 --- a/src/platforms/apple/core/physical-device-control.ts +++ b/src/platforms/apple/core/physical-device-control.ts @@ -32,9 +32,12 @@ import { import { runXcrun } from './tool-provider.ts'; export type IosPhysicalDeviceBackend = 'coredevice' | 'xctest'; -export type IosPhysicalDeviceRunnerTransport = - | { kind: 'network'; tunnelIp: string | null } - | { kind: 'usbmux' }; +/** + * Only the CoreDevice tunnel address is resolved here. Which transport a runner + * command uses is decided by the route resolver, which reaches this at all only + * after usbmux has reported the device unattached. + */ +export type IosPhysicalDeviceTunnel = { tunnelIp: string | null }; type IosPhysicalDeviceLaunchOptions = { payloadUrl?: string; @@ -75,10 +78,7 @@ export type IosPhysicalDeviceControl = { outPath: string, timeoutMs?: number, ): Promise; - resolveRunnerTransport( - device: DeviceInfo, - timeoutBudgetMs?: number, - ): Promise; + resolveTunnel(device: DeviceInfo, timeoutBudgetMs?: number): Promise; }; const CONTROLS: Record = { @@ -94,8 +94,7 @@ const CONTROLS: Record = { resolveAppProcesses: resolveCoreDeviceAppProcesses, captureScreenshot: captureCoreDeviceScreenshot, copyRunnerFile: copyCoreDeviceRunnerFile, - resolveRunnerTransport: async (device, timeoutBudgetMs) => ({ - kind: 'network', + resolveTunnel: async (device, timeoutBudgetMs) => ({ tunnelIp: await resolveCoreDeviceTunnelIp(device, timeoutBudgetMs), }), }, @@ -111,7 +110,7 @@ const CONTROLS: Record = { resolveAppProcesses: rejectXctestProcessLookup, captureScreenshot: captureXctestDeviceScreenshot, copyRunnerFile: rejectXctestRunnerFileCopy, - resolveRunnerTransport: async () => ({ kind: 'usbmux' }), + resolveTunnel: rejectXctestTunnelLookup, }, }; @@ -159,6 +158,18 @@ async function rejectXctestProcessLookup(device: DeviceInfo): Promise { ); } +async function rejectXctestTunnelLookup(device: DeviceInfo): Promise { + throw new AppError( + 'UNSUPPORTED_OPERATION', + 'XCTest-backed physical iOS devices have no CoreDevice tunnel.', + { + deviceId: device.id, + backend: 'xctest', + hint: 'Connect the device by cable so it is reachable through usbmux.', + }, + ); +} + async function rejectXctestRunnerFileCopy(device: DeviceInfo): Promise { throw new AppError( 'UNSUPPORTED_OPERATION', diff --git a/src/platforms/apple/core/runner/runner-command-route.ts b/src/platforms/apple/core/runner/runner-command-route.ts index 84fc624be0..c2c5e608fd 100644 --- a/src/platforms/apple/core/runner/runner-command-route.ts +++ b/src/platforms/apple/core/runner/runner-command-route.ts @@ -61,11 +61,7 @@ export function createRunnerCommandRouteResolver(device: DeviceInfo, port: numbe return buildNetworkRoute(device, port, requestTunnelIp, false); } } - const transport = await control.resolveRunnerTransport(device, timeoutBudgetMs); - if (transport.kind === 'usbmux') { - return buildUsbmuxRoute(device, port); - } - const tunnelIp = transport.tunnelIp; + const { tunnelIp } = await control.resolveTunnel(device, timeoutBudgetMs); requestTunnelIp = tunnelIp; if (tunnelIp) writeDeviceTunnelIpCache(device.id, tunnelIp); return buildNetworkRoute(device, port, tunnelIp, false);