Skip to content
Open
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
15 changes: 10 additions & 5 deletions src/platforms/apple/core/__tests__/physical-device-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
},
);
});

Expand Down
31 changes: 21 additions & 10 deletions src/platforms/apple/core/physical-device-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,10 +78,7 @@ export type IosPhysicalDeviceControl = {
outPath: string,
timeoutMs?: number,
): Promise<void>;
resolveRunnerTransport(
device: DeviceInfo,
timeoutBudgetMs?: number,
): Promise<IosPhysicalDeviceRunnerTransport>;
resolveTunnel(device: DeviceInfo, timeoutBudgetMs?: number): Promise<IosPhysicalDeviceTunnel>;
};

const CONTROLS: Record<IosPhysicalDeviceBackend, IosPhysicalDeviceControl> = {
Expand All @@ -94,8 +94,7 @@ const CONTROLS: Record<IosPhysicalDeviceBackend, IosPhysicalDeviceControl> = {
resolveAppProcesses: resolveCoreDeviceAppProcesses,
captureScreenshot: captureCoreDeviceScreenshot,
copyRunnerFile: copyCoreDeviceRunnerFile,
resolveRunnerTransport: async (device, timeoutBudgetMs) => ({
kind: 'network',
resolveTunnel: async (device, timeoutBudgetMs) => ({
tunnelIp: await resolveCoreDeviceTunnelIp(device, timeoutBudgetMs),
}),
},
Expand All @@ -111,7 +110,7 @@ const CONTROLS: Record<IosPhysicalDeviceBackend, IosPhysicalDeviceControl> = {
resolveAppProcesses: rejectXctestProcessLookup,
captureScreenshot: captureXctestDeviceScreenshot,
copyRunnerFile: rejectXctestRunnerFileCopy,
resolveRunnerTransport: async () => ({ kind: 'usbmux' }),
resolveTunnel: rejectXctestTunnelLookup,
},
};

Expand Down Expand Up @@ -159,6 +158,18 @@ async function rejectXctestProcessLookup(device: DeviceInfo): Promise<never> {
);
}

async function rejectXctestTunnelLookup(device: DeviceInfo): Promise<never> {
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<never> {
throw new AppError(
'UNSUPPORTED_OPERATION',
Expand Down
6 changes: 1 addition & 5 deletions src/platforms/apple/core/runner/runner-command-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading