diff --git a/src/tests/tools/app-management/resolve-app-id.test.ts b/src/tests/tools/app-management/resolve-app-id.test.ts index 619b1867..e3236c72 100644 --- a/src/tests/tools/app-management/resolve-app-id.test.ts +++ b/src/tests/tools/app-management/resolve-app-id.test.ts @@ -4,13 +4,15 @@ const rehydratedDriver = {rehydrated: true}; const mockResolveDriver = jest.fn<(sessionId?: string) => Promise>(); const mockListAppsFromDevice = jest.fn<(...args: any[]) => Promise<{packageName: string; appName: string}[]>>(); +const mockGetPlatformName = jest.fn<() => string>(); +const mockIsXCUITestDriverSession = jest.fn<() => boolean>(); jest.unstable_mockModule('../../../session-store', () => ({ // The in-memory cache is empty after an MCP process recycle. getDriver: jest.fn(() => null), getSessionId: jest.fn(() => undefined), - getPlatformName: jest.fn(() => 'Android'), - isXCUITestDriverSession: jest.fn(() => false), + getPlatformName: mockGetPlatformName, + isXCUITestDriverSession: mockIsXCUITestDriverSession, PLATFORM: {ios: 'iOS', android: 'Android'}, })); @@ -26,12 +28,14 @@ jest.unstable_mockModule('../../../tools/app-management/list-apps.js', () => ({ const {resolveAppId} = await import('../../../tools/app-management/resolve-app-id.js'); -describe('resolveAppId on a persisted session', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockListAppsFromDevice.mockResolvedValue([{packageName: 'com.example.calc', appName: 'Calculator'}]); - }); +beforeEach(() => { + jest.clearAllMocks(); + mockGetPlatformName.mockReturnValue('Android'); + mockIsXCUITestDriverSession.mockReturnValue(false); + mockListAppsFromDevice.mockResolvedValue([{packageName: 'com.example.calc', appName: 'Calculator'}]); +}); +describe('resolveAppId on a persisted session', () => { test('rehydrates the session instead of failing on an empty driver cache', async () => { mockResolveDriver.mockResolvedValue({ok: true, driver: rehydratedDriver}); @@ -48,3 +52,30 @@ describe('resolveAppId on a persisted session', () => { expect(mockListAppsFromDevice).not.toHaveBeenCalled(); }); }); + +describe('resolveAppId on a real iOS device', () => { + const deviceDriver = {isSimulator: () => false}; + + beforeEach(() => { + mockGetPlatformName.mockReturnValue('iOS'); + mockIsXCUITestDriverSession.mockReturnValue(true); + mockResolveDriver.mockResolvedValue({ok: true, driver: deviceDriver}); + }); + + test('keeps the apps that were listed when only one app type fails', async () => { + mockListAppsFromDevice + .mockResolvedValueOnce([{packageName: 'com.example.calc', appName: 'Calculator'}]) + .mockRejectedValueOnce(new Error('System app list unavailable')); + + await expect(resolveAppId('Calculator', 'device-1')).resolves.toBe('com.example.calc'); + }); + + test('reports the device failure instead of caching an empty app list', async () => { + mockListAppsFromDevice.mockRejectedValue(new Error('Could not list applications on the device')); + + await expect(resolveAppId('Calculator', 'device-2')).rejects.toThrow(/Could not list applications/); + + mockListAppsFromDevice.mockResolvedValue([{packageName: 'com.example.calc', appName: 'Calculator'}]); + await expect(resolveAppId('Calculator', 'device-2')).resolves.toBe('com.example.calc'); + }); +}); diff --git a/src/tools/app-management/resolve-app-id.ts b/src/tools/app-management/resolve-app-id.ts index a6b556e0..e237af0c 100644 --- a/src/tools/app-management/resolve-app-id.ts +++ b/src/tools/app-management/resolve-app-id.ts @@ -107,6 +107,10 @@ async function getInstalledApps(sessionId?: string): Promise<{packageName: strin listAppsFromDevice(driver, 'User'), listAppsFromDevice(driver, 'System'), ]); + const failures = results.filter((result) => result.status === 'rejected'); + if (failures.length === results.length) { + throw failures[0].reason; + } const seen = new Set(); apps = []; for (const result of results) {