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
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,22 @@ test('ensureCoreDeviceReady names the connecting tunnel state as the reason it r
assert.match(error.message, /not ready for automation/i);
});

test('ensureCoreDeviceReady refuses a device CoreDevice cannot reach', async () => {
const error = await expectReadyFailure(() => readyPayload('unavailable'));
assert.equal(error.details?.kind, 'not_ready');
assert.equal(error.details?.tunnelState, 'unavailable');
assert.match(error.message, /not ready for automation/i);
});

test('ensureCoreDeviceReady passes a device whose tunnel opens on demand', async () => {
await withCoreDeviceProbe(
() => readyPayload('disconnected'),
async () => {
await ensureCoreDeviceReady(IOS_DEVICE);
},
);
});

test('ensureCoreDeviceReady reports a failed details command with the device hint', async () => {
const error = await expectReadyFailure(() => ({
exitCode: 1,
Expand Down
4 changes: 4 additions & 0 deletions packages/platform-apple/src/core/physical-device-constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export const IOS_DEVICE_READY_TIMEOUT_MS = 15_000;
export const IOS_DEVICE_READY_COMMAND_TIMEOUT_BUFFER_MS = 3_000;

export function isCoreDeviceReachable(tunnelState: unknown): boolean {
return typeof tunnelState !== 'string' || tunnelState.toLowerCase() !== 'unavailable';
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import {
IOS_DEVICE_READY_COMMAND_TIMEOUT_BUFFER_MS,
IOS_DEVICE_READY_TIMEOUT_MS,
isCoreDeviceReachable,
} from './physical-device-constants.ts';

const IOS_RUNNER_DEVICE_INFO_TIMEOUT_MS = 10_000;
Expand Down Expand Up @@ -72,6 +73,14 @@ export async function ensureCoreDeviceReady(
hint: 'Device tunnel is still connecting. Keep the device unlocked and connected by cable until it is fully available in Xcode Devices, then retry.',
});
}
if (!isCoreDeviceReachable(tunnelState)) {
throw new AppError('COMMAND_FAILED', 'iOS device is not ready for automation', {
kind: 'not_ready',
deviceId: device.id,
tunnelState,
hint: 'CoreDevice cannot reach the device. Connect it by cable or to the same network as this Mac, unlock it, and wait until Xcode Devices lists it as available, then retry.',
});
}
return;
}
throw new AppError(
Expand Down
21 changes: 21 additions & 0 deletions packages/platform-apple/src/inventory-classification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,27 @@ test('devicectl records map to normalized physical Apple devices', () => {
);
});

test('devicectl records report a paired device CoreDevice cannot reach as not booted', () => {
const record = (tunnelState?: string) => ({
hardwareProperties: { platform: 'iOS', productType: 'iPhone18,1', udid: 'phone-1' },
...(tunnelState === undefined ? {} : { connectionProperties: { tunnelState } }),
});
assert.equal(mapDevicectlAppleDevice(record('unavailable'))?.booted, false);
assert.equal(mapDevicectlAppleDevice(record('Unavailable'))?.booted, false);
// CoreDevice opens a tunnel on demand, so an idle device it can reach is still usable.
assert.equal(mapDevicectlAppleDevice(record('disconnected'))?.booted, true);
assert.equal(mapDevicectlAppleDevice(record('connected'))?.booted, true);
assert.equal(mapDevicectlAppleDevice(record())?.booted, true);
// devicectl JSON is not validated at runtime, so a malformed state must not break the listing.
assert.equal(
mapDevicectlAppleDevice({
hardwareProperties: { platform: 'iOS', productType: 'iPhone18,1', udid: 'phone-1' },
connectionProperties: { tunnelState: 3 as unknown as string },
})?.booted,
true,
);
});

test('devicectl records report the marketing model and OS version', () => {
const devicectlRecord = {
identifier: '3F1DAD0D-212F-44F2-B41E-C390F96140BE',
Expand Down
3 changes: 2 additions & 1 deletion packages/platform-apple/src/inventory-classification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AppleOS, DeviceInfo, DeviceTarget } from '@agent-device/kernel/device';
import { isCoreDeviceReachable } from './core/physical-device-constants.ts';

const APPLE_PRODUCT_TYPE_PATTERN = /^(iphone|ipad|ipod|appletv|realitydevice)/i;
const APPLE_IPAD_PATTERN = /ipad/i;
Expand Down Expand Up @@ -128,6 +129,6 @@ export function mapDevicectlAppleDevice(device: DevicectlAppleDevice): DeviceInf
...(model ? { model } : {}),
...(osVersion ? { osVersion } : {}),
iosPhysicalDeviceBackend: 'coredevice',
booted: true,
booted: isCoreDeviceReachable(device.connectionProperties?.tunnelState),
};
}
2 changes: 1 addition & 1 deletion src/commands/schema/cli-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ Discovery:

iOS physical-device prerequisites:
Xcode, xcrun xcdevice, and xcrun xctrace must be available from the selected Xcode.
The device must be paired/trusted, connected, unlocked when needed, and have Developer Mode enabled.
The device must be paired/trusted, connected, unlocked when needed, and have Developer Mode enabled. A paired device that CoreDevice cannot reach (devicectl tunnelState unavailable: unplugged or off the network) is listed with booted: false, and commands refuse it with details.kind not_ready and details.tunnelState unavailable.
Modern devices visible to devicectl use CoreDevice. Older devices visible only to xctrace use the XCTest backend automatically.
XCTest-backed devices must already have the target app installed and should be opened by bundle ID; app inventory, install/reinstall, logs, performance sampling, recording, deep links, and launch arguments require CoreDevice.
XCTest-backed runner commands travel through macOS usbmuxd; keep the trusted device connected by cable.
Expand Down
1 change: 1 addition & 0 deletions website/docs/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Run `agent-device help physical-device` for the same setup guidance from the CLI

- Xcode is installed with `xcrun devicectl` and `xcrun xctrace` available.
- The device is paired, trusted, connected, unlocked when needed, and listed by `xcrun devicectl list devices`.
A paired device that CoreDevice cannot reach (`tunnelState: unavailable`, for example unplugged and off the network) is listed by `devices` with `booted: false`. Commands refuse it with `details.kind: not_ready` and `details.tunnelState: unavailable`.
- Developer Mode is on in the device's Settings.
- The Apple runner must be signed before commands can run on the device. Start with Automatic Signing in Xcode and these environment variables; add the others below only when needed:
- `AGENT_DEVICE_IOS_TEAM_ID`
Expand Down
Loading