Skip to content

Commit 23e34fd

Browse files
committed
fix(setup): Tolerate device fallback discovery failures
Return an empty device list when both devicectl and xctrace discovery fail so setup can continue with no default device instead of aborting. Add regression coverage for the double-failure discovery path.
1 parent 84e3f14 commit 23e34fd

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

src/cli/commands/__tests__/setup.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,68 @@ sessionDefaults:
679679
expect(parsed.sessionDefaults?.deviceId).toBeUndefined();
680680
});
681681

682+
it('continues setup with no default device when both discovery commands fail', async () => {
683+
const { fs, getStoredConfig } = createSetupFs();
684+
685+
const executor: CommandExecutor = async (command) => {
686+
if (command[0] === 'xcrun' && command[1] === 'devicectl') {
687+
throw new Error('devicectl unavailable');
688+
}
689+
690+
if (command[0] === 'xcrun' && command[1] === 'xctrace') {
691+
return createMockCommandResponse({ success: false, output: '', error: 'xctrace failed' });
692+
}
693+
694+
if (command.includes('--json')) {
695+
return createMockCommandResponse({
696+
success: true,
697+
output: JSON.stringify({
698+
devices: {
699+
'iOS 17.0': [
700+
{
701+
name: 'iPhone 15',
702+
udid: 'SIM-1',
703+
state: 'Shutdown',
704+
isAvailable: true,
705+
},
706+
],
707+
},
708+
}),
709+
});
710+
}
711+
712+
return createMockCommandResponse({
713+
success: true,
714+
output: `Information about workspace "App":\n Schemes:\n App`,
715+
});
716+
};
717+
718+
const prompter: Prompter = {
719+
selectOne: async <T>(opts: { options: Array<{ value: T }> }) => opts.options[0].value,
720+
selectMany: async <T>(opts: { options: Array<{ value: T }> }) => {
721+
const loggingOption = opts.options.find((option) => option.value === ('logging' as T));
722+
return loggingOption ? [loggingOption.value] : opts.options.map((option) => option.value);
723+
},
724+
confirm: async (opts: { defaultValue: boolean }) => opts.defaultValue,
725+
};
726+
727+
await runSetupWizard({
728+
cwd,
729+
fs,
730+
executor,
731+
prompter,
732+
quietOutput: true,
733+
});
734+
735+
const parsed = parseYaml(getStoredConfig()) as {
736+
sessionDefaults?: Record<string, unknown>;
737+
};
738+
739+
expect(parsed.sessionDefaults?.deviceId).toBeUndefined();
740+
expect(parsed.sessionDefaults?.simulatorId).toBeUndefined();
741+
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
742+
});
743+
682744
it('continues setup with no default simulator when no simulators are available', async () => {
683745
const { fs, getStoredConfig } = createSetupFs();
684746

src/cli/commands/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ async function listAvailableDevices(
574574
);
575575

576576
if (!fallbackResult.success) {
577-
throw new Error(`Failed to list devices: ${fallbackResult.error}`);
577+
return [];
578578
}
579579

580580
return parseXctraceDevices(fallbackResult.output);

0 commit comments

Comments
 (0)