diff --git a/src/core/platform-inventory.test.ts b/src/core/platform-inventory.test.ts index d6b85bf02b..11816338a7 100644 --- a/src/core/platform-inventory.test.ts +++ b/src/core/platform-inventory.test.ts @@ -2,14 +2,29 @@ import assert from 'node:assert/strict'; import { test, vi } from 'vitest'; import type { DeviceInfo } from '@agent-device/kernel/device'; -const { listVegaDevices } = vi.hoisted(() => ({ - listVegaDevices: vi.fn(), -})); +const { listVegaDevices, listAndroidDevices, listAppleDevices, listLinuxDevices } = vi.hoisted( + () => ({ + listVegaDevices: vi.fn(), + listAndroidDevices: vi.fn(), + listAppleDevices: vi.fn(), + listLinuxDevices: vi.fn(), + }), +); vi.mock('../platforms/vega/devices.ts', () => ({ listVegaDevices, })); +vi.mock('../platforms/android/devices.ts', () => ({ + listAndroidDevices, +})); +vi.mock('../platforms/apple/core/devices.ts', () => ({ + listAppleDevices, +})); +vi.mock('../platforms/linux/devices.ts', () => ({ + listLinuxDevices, +})); +import { LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS } from '@agent-device/contracts/device'; import { listLocalDeviceInventory } from './platform-inventory.ts'; const VEGA_EMULATOR: DeviceInfo = { @@ -33,3 +48,63 @@ test('explicit Vega inventory delegates to the Vega device module', async () => assert.deepEqual(result, [VEGA_EMULATOR]); assert.deepEqual(listVegaDevices.mock.calls[0], []); }); + +test('probes every platform concurrently and keeps selector order in the result', async () => { + // Sequential awaits made an unfiltered lookup cost the sum of every + // toolchain probe. Each stub records when it starts and resolves only once + // all four have started, so this deadlocks unless they genuinely overlap. + const PLATFORM_COUNT = LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.length; + let started = 0; + let allStarted!: () => void; + const everyProbeStarted = new Promise((resolve) => { + allStarted = resolve; + }); + const gate = async () => { + started += 1; + if (started === PLATFORM_COUNT) allStarted(); + await everyProbeStarted; + }; + + listAndroidDevices.mockImplementation(async () => { + await gate(); + return [device('android', 'android-1')]; + }); + listAppleDevices.mockImplementation(async () => { + await gate(); + return [device('apple', 'apple-1')]; + }); + listVegaDevices.mockImplementation(async () => { + await gate(); + return [device('vega', 'vega-1')]; + }); + listLinuxDevices.mockImplementation(async () => { + await gate(); + return [device('linux', 'linux-1')]; + }); + + const result = await listLocalDeviceInventory({}); + + assert.equal(started, PLATFORM_COUNT); + assert.deepEqual( + result.map((entry) => entry.id), + ['android-1', 'apple-1', 'vega-1', 'linux-1'], + ); +}); + +test('a failing platform probe does not drop the devices found by the others', async () => { + listAndroidDevices.mockRejectedValue(new Error('adb exploded')); + listAppleDevices.mockResolvedValue([device('apple', 'apple-1')]); + listVegaDevices.mockRejectedValue(new Error('vega exploded')); + listLinuxDevices.mockResolvedValue([device('linux', 'linux-1')]); + + const result = await listLocalDeviceInventory({}); + + assert.deepEqual( + result.map((entry) => entry.id), + ['apple-1', 'linux-1'], + ); +}); + +function device(platform: DeviceInfo['platform'], id: string): DeviceInfo { + return { platform, id, name: id, kind: 'device', booted: true }; +} diff --git a/src/core/platform-inventory.ts b/src/core/platform-inventory.ts index c0c05239cd..f1c96892e1 100644 --- a/src/core/platform-inventory.ts +++ b/src/core/platform-inventory.ts @@ -43,15 +43,27 @@ export async function listLocalDeviceInventory( }); } - const devices: DeviceInfo[] = []; - // Linux local device is appended last so it does not displace - // connected Android/Apple devices in implicit auto-selection. - for (const platform of LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS) { - try { - devices.push(...(await listLocalDeviceInventory({ ...request, platform }))); - } catch {} - } - return devices; + // Probed concurrently: each platform shells out to its own toolchain, and + // awaiting them in turn made an unfiltered lookup cost their sum — measured + // at 6.7s on a host with the Apple, Android and Vega toolchains installed, + // most of it spent enumerating platforms the request could not target. + // + // Results are still concatenated in selector order, so the Linux local device + // stays last and does not displace connected Android/Apple devices in + // implicit auto-selection. + const perPlatform = await Promise.all( + LOCAL_DEVICE_INVENTORY_PLATFORM_SELECTORS.map(async (platform) => { + try { + const listed = await listLocalDeviceInventory({ ...request, platform }); + // A platform that answers with anything but a list contributes nothing, + // exactly as before: spreading a non-array used to throw into the catch. + return Array.isArray(listed) ? listed : []; + } catch { + return []; + } + }), + ); + return perPlatform.flat(); } export function resolveAndroidDiscoverySerialAllowlist(