diff --git a/packages/kernel/src/device.ts b/packages/kernel/src/device.ts index 16e3c68388..8b5f172118 100644 --- a/packages/kernel/src/device.ts +++ b/packages/kernel/src/device.ts @@ -409,8 +409,7 @@ function resolveDeviceByName( deviceName: string | undefined, ): DeviceInfo | undefined { if (!deviceName) return undefined; - const normalizedName = normalizeDeviceName(deviceName); - const match = candidates.find((device) => normalizeDeviceName(device.name) === normalizedName); + const match = candidates.find((device) => matchesDeviceNameSelector(device, deviceName)); if (!match) { const hint = deviceIdentityMistakenForNameHint(candidates, deviceName); throw new AppError( @@ -570,10 +569,7 @@ function matchesExplicitDeviceSelector(device: DeviceInfo, selector: DeviceSelec ) { return false; } - if ( - selector.deviceName && - normalizeDeviceName(device.name) !== normalizeDeviceName(selector.deviceName) - ) { + if (selector.deviceName && !matchesDeviceNameSelector(device, selector.deviceName)) { return false; } return true; @@ -610,6 +606,18 @@ function throwNoDevicesFound(selector: DeviceSelector, context: DeviceSelectionC throw new AppError('DEVICE_NOT_FOUND', 'No devices found', { selector }); } +/** + * Whether a `--device` name selects `device`. Selection and session-binding checks share this + * matcher so a name that picked a device keeps matching it, e.g. AVD `Pixel_9_API_37` for the + * displayed `Pixel 9 API 37`. + */ +export function matchesDeviceNameSelector( + device: Pick, + deviceName: string, +): boolean { + return normalizeDeviceName(device.name) === normalizeDeviceName(deviceName); +} + function normalizeDeviceName(value: string): string { return value.toLowerCase().replaceAll('_', ' ').replaceAll(/\s+/g, ' ').trim(); } diff --git a/src/daemon/__tests__/session-selector.test.ts b/src/daemon/__tests__/session-selector.test.ts index 961b9ffdda..a224a7a059 100644 --- a/src/daemon/__tests__/session-selector.test.ts +++ b/src/daemon/__tests__/session-selector.test.ts @@ -2,6 +2,7 @@ import { test } from 'vitest'; import assert from 'node:assert/strict'; import { assertSessionSelectorMatches } from '../session-selector.ts'; import { AppError } from '@agent-device/kernel/errors'; +import { resolveDevice } from '@agent-device/kernel/device'; import type { SessionRef, SessionState } from '../session-state.ts'; function makeSession(overrides?: Partial): SessionState { @@ -132,6 +133,20 @@ test('accepts matching device selector (case-insensitive)', () => { ); }); +test('accepts the AVD name selector that selected the bound device', async () => { + const avdName = 'Pixel_9_API_37'; + const device = await resolveDevice( + [{ platform: 'android', id: 'emulator-5554', name: 'Pixel 9 API 37', kind: 'emulator' }], + { platform: 'android', deviceName: avdName }, + ); + assert.doesNotThrow(() => + assertSessionSelectorMatches(ref(makeSession({ device })), { + platform: 'android', + device: avdName, + }), + ); +}); + test('rejects mismatched target selector', () => { const session = makeSession(); assert.throws( diff --git a/src/daemon/session-selector.ts b/src/daemon/session-selector.ts index 7b134310cd..da80b1e7dd 100644 --- a/src/daemon/session-selector.ts +++ b/src/daemon/session-selector.ts @@ -4,6 +4,7 @@ import type { SessionRef, SessionState } from './session-state.ts'; import { isIosFamily, isSerialAddressablePlatform, + matchesDeviceNameSelector, matchesPlatformSelector, } from '@agent-device/kernel/device'; import { parseSerialAllowlist } from '@agent-device/kernel/device-isolation'; @@ -71,7 +72,7 @@ export function listSessionSelectorConflicts( mismatches.push({ key: 'serial', value: flags.serial }); } - if (flags.device && flags.device.trim().toLowerCase() !== device.name.trim().toLowerCase()) { + if (flags.device && !matchesDeviceNameSelector(device, flags.device)) { mismatches.push({ key: 'device', value: flags.device }); }