Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
idb-device-detection.test.ts10.1 kB
import { resolveIdbUdid, validateDeviceReady, validateTargetBooted, } from '../../../src/utils/idb-device-detection.js'; import { IDBTargetCache } from '../../../src/state/idb-target-cache.js'; import { executeCommand } from '../../../src/utils/command.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; // Mock dependencies jest.mock('../../../src/state/idb-target-cache.js'); jest.mock('../../../src/utils/command.js'); const mockIDBTargetCache = IDBTargetCache as jest.Mocked<typeof IDBTargetCache>; const mockExecuteCommand = executeCommand as jest.MockedFunction<typeof executeCommand>; describe('idb-device-detection', () => { beforeEach(() => { jest.clearAllMocks(); }); describe('resolveIdbUdid', () => { it('should return explicit UDID when provided', async () => { const result = await resolveIdbUdid('explicit-udid-123'); expect(result).toBe('explicit-udid-123'); expect(mockIDBTargetCache.getLastUsedTarget).not.toHaveBeenCalled(); }); it('should trim explicit UDID', async () => { const result = await resolveIdbUdid(' udid-with-spaces '); expect(result).toBe('udid-with-spaces'); }); it('should auto-detect UDID when not provided', async () => { mockIDBTargetCache.getLastUsedTarget = jest.fn().mockResolvedValue({ udid: 'auto-detected-udid', name: 'iPhone 16 Pro', type: 'simulator', state: 'Booted', }); const result = await resolveIdbUdid(); expect(result).toBe('auto-detected-udid'); expect(mockIDBTargetCache.getLastUsedTarget).toHaveBeenCalled(); }); it('should auto-detect when empty string provided', async () => { mockIDBTargetCache.getLastUsedTarget = jest.fn().mockResolvedValue({ udid: 'auto-detected-udid', name: 'iPhone 16 Pro', type: 'simulator', state: 'Booted', }); const result = await resolveIdbUdid(''); expect(result).toBe('auto-detected-udid'); expect(mockIDBTargetCache.getLastUsedTarget).toHaveBeenCalled(); }); it('should throw error when no UDID provided and no booted targets', async () => { mockIDBTargetCache.getLastUsedTarget = jest.fn().mockResolvedValue(undefined); await expect(resolveIdbUdid()).rejects.toThrow(McpError); await expect(resolveIdbUdid()).rejects.toThrow( 'No UDID provided and no booted targets found' ); }); it('should include helpful error message when no targets available', async () => { mockIDBTargetCache.getLastUsedTarget = jest.fn().mockResolvedValue(undefined); await expect(resolveIdbUdid()).rejects.toThrow( 'Boot a simulator or device first, or provide explicit UDID parameter' ); }); }); describe('validateDeviceReady', () => { it('should skip validation for simulators', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Booted', }); await expect(validateDeviceReady('sim-udid-123')).resolves.toBeUndefined(); expect(mockExecuteCommand).not.toHaveBeenCalled(); }); it('should validate companion for physical devices', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Booted', connectionType: 'usb', }); mockExecuteCommand.mockResolvedValueOnce({ code: 0, stdout: 'Companion reachable', stderr: '', }); await expect(validateDeviceReady('device-udid-123')).resolves.toBeUndefined(); expect(mockExecuteCommand).toHaveBeenCalledWith( expect.stringContaining('idb connect device-udid-123 --check-companion'), expect.any(Object) ); }); it('should throw error when device companion not ready', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Booted', connectionType: 'usb', }); mockExecuteCommand.mockResolvedValue({ code: 1, stdout: '', stderr: 'Companion not reachable', }); await expect(validateDeviceReady('device-udid-123')).rejects.toThrow(McpError); await expect(validateDeviceReady('device-udid-123')).rejects.toThrow( 'Physical device "iPhone 15 Pro" (device-udid-123) not ready' ); }); it('should include troubleshooting for USB devices', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Booted', connectionType: 'usb', }); mockExecuteCommand.mockResolvedValueOnce({ code: 1, stdout: '', stderr: 'Companion not reachable', }); await expect(validateDeviceReady('device-udid-123')).rejects.toThrow( 'Device connected via USB ✓' ); }); it('should include troubleshooting for WiFi devices', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Booted', connectionType: 'wifi', }); mockExecuteCommand.mockResolvedValueOnce({ code: 1, stdout: '', stderr: 'Companion not reachable', }); await expect(validateDeviceReady('device-udid-123')).rejects.toThrow( 'Device connected via WiFi' ); }); it('should handle unexpected errors during validation', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Booted', }); mockExecuteCommand.mockRejectedValue(new Error('Network error')); await expect(validateDeviceReady('device-udid-123')).rejects.toThrow(McpError); await expect(validateDeviceReady('device-udid-123')).rejects.toThrow( 'Failed to validate device companion' ); }); }); describe('validateTargetBooted', () => { it('should pass validation for booted simulator', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Booted', }); const result = await validateTargetBooted('sim-udid-123'); expect(result).toEqual({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Booted', }); }); it('should pass validation for booted device', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Booted', }); const result = await validateTargetBooted('device-udid-123'); expect(result.state).toBe('Booted'); }); it('should throw error for shutdown simulator', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Shutdown', }); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow(McpError); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow( 'Target "iPhone 16 Pro" (sim-udid-123) is not booted' ); }); it('should include current state in error message', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Shutdown', }); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow('Current state: Shutdown'); }); it('should suggest simctl-boot for simulators', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Shutdown', }); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow( 'Simulator: simctl-boot sim-udid-123' ); }); it('should suggest wake/unlock for physical devices', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'device-udid-123', name: 'iPhone 15 Pro', type: 'device', state: 'Shutdown', }); await expect(validateTargetBooted('device-udid-123')).rejects.toThrow( 'Device: Wake device and unlock' ); }); it('should suggest idb-targets tool for listing booted targets', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Shutdown', }); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow( 'idb-targets to list available booted targets' ); }); }); describe('Error Types', () => { it('should throw McpError for missing UDID', async () => { mockIDBTargetCache.getLastUsedTarget = jest.fn().mockResolvedValue(undefined); await expect(resolveIdbUdid()).rejects.toThrow(McpError); await expect(resolveIdbUdid()).rejects.toThrow('No UDID provided'); }); it('should throw McpError for not booted target', async () => { mockIDBTargetCache.getTarget = jest.fn().mockResolvedValue({ udid: 'sim-udid-123', name: 'iPhone 16 Pro', type: 'simulator', state: 'Shutdown', }); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow(McpError); await expect(validateTargetBooted('sim-udid-123')).rejects.toThrow('not booted'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/conorluddy/xc-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server