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
1 change: 1 addition & 0 deletions skills/agent-device/references/session-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ agent-device devices --platform android --android-device-allowlist emulator-5554

- Scope is applied before selectors (`--device`, `--udid`, `--serial`).
- If selector target is outside scope, resolution fails with `DEVICE_NOT_FOUND`.
- If the scoped iOS simulator set is empty (first-run), the error includes the set path and a suggested `xcrun simctl --set <path> create ...` command.
- With iOS simulator-set scope enabled, iOS physical devices are not enumerated.
- Environment equivalents:
- `AGENT_DEVICE_IOS_SIMULATOR_DEVICE_SET` (compat: `IOS_SIMULATOR_DEVICE_SET`)
Expand Down
4 changes: 2 additions & 2 deletions src/core/dispatch-resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function resolveTargetDevice(flags: ResolveDeviceFlags): Promise<De

if (selector.platform === 'ios') {
const devices = await listIosDevices({ simulatorSetPath: iosSimulatorSetPath });
return await selectDevice(devices, selector);
return await selectDevice(devices, selector, { simulatorSetPath: iosSimulatorSetPath });
}

const devices: DeviceInfo[] = [];
Expand All @@ -55,7 +55,7 @@ export async function resolveTargetDevice(flags: ResolveDeviceFlags): Promise<De
} catch {
// ignore
}
return await selectDevice(devices, selector);
return await selectDevice(devices, selector, { simulatorSetPath: iosSimulatorSetPath });
},
{
platform: normalizedPlatform,
Expand Down
48 changes: 47 additions & 1 deletion src/utils/__tests__/device.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { normalizePlatformSelector, resolveApplePlatformName } from '../device.ts';
import { normalizePlatformSelector, resolveApplePlatformName, selectDevice } from '../device.ts';
import type { DeviceInfo } from '../device.ts';
import { AppError } from '../errors.ts';

test('normalizePlatformSelector resolves apple alias to ios', () => {
assert.equal(normalizePlatformSelector('apple'), 'ios');
Expand All @@ -14,3 +16,47 @@ test('resolveApplePlatformName resolves tv targets to tvOS', () => {
assert.equal(resolveApplePlatformName('mobile'), 'iOS');
assert.equal(resolveApplePlatformName(undefined), 'iOS');
});

test('selectDevice throws DEVICE_NOT_FOUND with scoped set guidance when simulatorSetPath is set and no devices found', async () => {
const setPath = '/path/to/sessions/abc/Simulators';
const err = await selectDevice([], { platform: 'ios' }, { simulatorSetPath: setPath }).catch((e) => e);
assert.ok(err instanceof AppError);
assert.equal(err.code, 'DEVICE_NOT_FOUND');
assert.match(err.message, /scoped simulator set/);
assert.equal(err.details?.simulatorSetPath, setPath);
assert.ok(typeof err.details?.hint === 'string');
assert.match(err.details.hint as string, /simctl --set/);
assert.match(err.details.hint as string, /create/);
});

test('selectDevice throws generic DEVICE_NOT_FOUND when no simulatorSetPath and no devices found', async () => {
const err = await selectDevice([], { platform: 'ios' }).catch((e) => e);
assert.ok(err instanceof AppError);
assert.equal(err.code, 'DEVICE_NOT_FOUND');
assert.equal(err.message, 'No devices found');
assert.equal(err.details?.simulatorSetPath, undefined);
});

test('selectDevice does not apply scoped set guidance for non-iOS platform with simulatorSetPath', async () => {
const setPath = '/path/to/sessions/abc/Simulators';
const err = await selectDevice([], { platform: 'android' }, { simulatorSetPath: setPath }).catch((e) => e);
assert.ok(err instanceof AppError);
assert.equal(err.code, 'DEVICE_NOT_FOUND');
assert.equal(err.message, 'No devices found');
assert.equal(err.details?.simulatorSetPath, undefined);
});

test('selectDevice applies scoped set guidance when no platform selector specified and simulatorSetPath is set', async () => {
const setPath = '/path/to/sessions/abc/Simulators';
const err = await selectDevice([], {}, { simulatorSetPath: setPath }).catch((e) => e);
assert.ok(err instanceof AppError);
assert.equal(err.code, 'DEVICE_NOT_FOUND');
assert.match(err.message, /scoped simulator set/);
assert.equal(err.details?.simulatorSetPath, setPath);
});

test('selectDevice returns a device when candidates are available', async () => {
const device: DeviceInfo = { platform: 'ios', id: 'abc123', name: 'iPhone 16', kind: 'simulator', booted: true };
const result = await selectDevice([device], { platform: 'ios' });
assert.equal(result.id, 'abc123');
});
13 changes: 13 additions & 0 deletions src/utils/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type DeviceSelector = {
serial?: string;
};

type DeviceSelectionContext = {
simulatorSetPath?: string;
};

export function normalizePlatformSelector(
platform: PlatformSelector | undefined,
): Platform | undefined {
Expand All @@ -39,6 +43,7 @@ export function resolveApplePlatformName(target: DeviceTarget | undefined): 'iOS
export async function selectDevice(
devices: DeviceInfo[],
selector: DeviceSelector,
context: DeviceSelectionContext = {},
): Promise<DeviceInfo> {
let candidates = devices;
const normalize = (value: string): string =>
Expand Down Expand Up @@ -76,6 +81,14 @@ export async function selectDevice(
if (candidates.length === 1) return candidates[0];

if (candidates.length === 0) {
const simulatorSetPath = context.simulatorSetPath;
if (simulatorSetPath && (!selector.platform || selector.platform === 'ios')) {
throw new AppError('DEVICE_NOT_FOUND', 'No devices found in the scoped simulator set', {
simulatorSetPath,
hint: `The simulator set at "${simulatorSetPath}" appears to be empty. Create a simulator first:\n xcrun simctl --set "${simulatorSetPath}" create "iPhone 16" com.apple.CoreSimulator.SimDeviceType.iPhone-16 com.apple.CoreSimulator.SimRuntime.iOS-18-0`,
selector,
});
}
throw new AppError('DEVICE_NOT_FOUND', 'No devices found', { selector });
}

Expand Down
Loading