Skip to content

Commit 08c46dc

Browse files
thymikeeclaude
andcommitted
fix: enrich DEVICE_NOT_FOUND error for empty scoped iOS simulator sets (#168)
When selectDevice finds no candidates and a simulatorSetPath context is provided for iOS, throw a detailed error including the set path and a suggested xcrun simctl --set ... create command to provision the first simulator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c691082 commit 08c46dc

3 files changed

Lines changed: 62 additions & 3 deletions

File tree

src/core/dispatch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function resolveTargetDevice(flags: CommandFlags): Promise<DeviceIn
7272

7373
if (selector.platform === 'ios') {
7474
const devices = await listIosDevices({ simulatorSetPath: iosSimulatorSetPath });
75-
return await selectDevice(devices, selector);
75+
return await selectDevice(devices, selector, { simulatorSetPath: iosSimulatorSetPath });
7676
}
7777

7878
const devices: DeviceInfo[] = [];
@@ -86,7 +86,7 @@ export async function resolveTargetDevice(flags: CommandFlags): Promise<DeviceIn
8686
} catch {
8787
// ignore
8888
}
89-
return await selectDevice(devices, selector);
89+
return await selectDevice(devices, selector, { simulatorSetPath: iosSimulatorSetPath });
9090
},
9191
{
9292
platform: normalizedPlatform,

src/utils/__tests__/device.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
3-
import { normalizePlatformSelector, resolveApplePlatformName } from '../device.ts';
3+
import { normalizePlatformSelector, resolveApplePlatformName, selectDevice } from '../device.ts';
4+
import type { DeviceInfo } from '../device.ts';
5+
import { AppError } from '../errors.ts';
46

57
test('normalizePlatformSelector resolves apple alias to ios', () => {
68
assert.equal(normalizePlatformSelector('apple'), 'ios');
@@ -14,3 +16,47 @@ test('resolveApplePlatformName resolves tv targets to tvOS', () => {
1416
assert.equal(resolveApplePlatformName('mobile'), 'iOS');
1517
assert.equal(resolveApplePlatformName(undefined), 'iOS');
1618
});
19+
20+
test('selectDevice throws DEVICE_NOT_FOUND with scoped set guidance when simulatorSetPath is set and no devices found', async () => {
21+
const setPath = '/path/to/sessions/abc/Simulators';
22+
const err = await selectDevice([], { platform: 'ios' }, { simulatorSetPath: setPath }).catch((e) => e);
23+
assert.ok(err instanceof AppError);
24+
assert.equal(err.code, 'DEVICE_NOT_FOUND');
25+
assert.match(err.message, /scoped simulator set/);
26+
assert.equal(err.details?.simulatorSetPath, setPath);
27+
assert.ok(typeof err.details?.hint === 'string');
28+
assert.match(err.details.hint as string, /simctl --set/);
29+
assert.match(err.details.hint as string, /create/);
30+
});
31+
32+
test('selectDevice throws generic DEVICE_NOT_FOUND when no simulatorSetPath and no devices found', async () => {
33+
const err = await selectDevice([], { platform: 'ios' }).catch((e) => e);
34+
assert.ok(err instanceof AppError);
35+
assert.equal(err.code, 'DEVICE_NOT_FOUND');
36+
assert.equal(err.message, 'No devices found');
37+
assert.equal(err.details?.simulatorSetPath, undefined);
38+
});
39+
40+
test('selectDevice does not apply scoped set guidance for non-iOS platform with simulatorSetPath', async () => {
41+
const setPath = '/path/to/sessions/abc/Simulators';
42+
const err = await selectDevice([], { platform: 'android' }, { simulatorSetPath: setPath }).catch((e) => e);
43+
assert.ok(err instanceof AppError);
44+
assert.equal(err.code, 'DEVICE_NOT_FOUND');
45+
assert.equal(err.message, 'No devices found');
46+
assert.equal(err.details?.simulatorSetPath, undefined);
47+
});
48+
49+
test('selectDevice applies scoped set guidance when no platform selector specified and simulatorSetPath is set', async () => {
50+
const setPath = '/path/to/sessions/abc/Simulators';
51+
const err = await selectDevice([], {}, { simulatorSetPath: setPath }).catch((e) => e);
52+
assert.ok(err instanceof AppError);
53+
assert.equal(err.code, 'DEVICE_NOT_FOUND');
54+
assert.match(err.message, /scoped simulator set/);
55+
assert.equal(err.details?.simulatorSetPath, setPath);
56+
});
57+
58+
test('selectDevice returns a device when candidates are available', async () => {
59+
const device: DeviceInfo = { platform: 'ios', id: 'abc123', name: 'iPhone 16', kind: 'simulator', booted: true };
60+
const result = await selectDevice([device], { platform: 'ios' });
61+
assert.equal(result.id, 'abc123');
62+
});

src/utils/device.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ type DeviceSelector = {
2525
serial?: string;
2626
};
2727

28+
type DeviceSelectionContext = {
29+
simulatorSetPath?: string;
30+
};
31+
2832
export function normalizePlatformSelector(
2933
platform: PlatformSelector | undefined,
3034
): Platform | undefined {
@@ -39,6 +43,7 @@ export function resolveApplePlatformName(target: DeviceTarget | undefined): 'iOS
3943
export async function selectDevice(
4044
devices: DeviceInfo[],
4145
selector: DeviceSelector,
46+
context: DeviceSelectionContext = {},
4247
): Promise<DeviceInfo> {
4348
let candidates = devices;
4449
const normalize = (value: string): string =>
@@ -76,6 +81,14 @@ export async function selectDevice(
7681
if (candidates.length === 1) return candidates[0];
7782

7883
if (candidates.length === 0) {
84+
const simulatorSetPath = context.simulatorSetPath;
85+
if (simulatorSetPath && (!selector.platform || selector.platform === 'ios')) {
86+
throw new AppError('DEVICE_NOT_FOUND', 'No devices found in the scoped simulator set', {
87+
simulatorSetPath,
88+
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`,
89+
selector,
90+
});
91+
}
7992
throw new AppError('DEVICE_NOT_FOUND', 'No devices found', { selector });
8093
}
8194

0 commit comments

Comments
 (0)