From a0bff7611ac251a8b708931634438f5f891a74c8 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 21:53:45 +0200 Subject: [PATCH 1/5] =?UTF-8?q?test:=20reproduce=20#341=20=E2=80=94=20a=20?= =?UTF-8?q?listed=20model's=20class=20comes=20from=20the=20listed=20profil?= =?UTF-8?q?e,=20not=20the=20one=20its=20name=20resolves=20to?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/device-profile-source.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/drivers/android/device-profile-source.test.ts b/src/drivers/android/device-profile-source.test.ts index 01571aef4..85d2dab13 100644 --- a/src/drivers/android/device-profile-source.test.ts +++ b/src/drivers/android/device-profile-source.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import type { DeviceClass } from "../../core/index.js"; import { MemoryFilesystem, ScriptedProcessRunner } from "../../ports/index.js"; import { BuiltinDeviceProfileSource, @@ -9,6 +10,7 @@ import { UserDeviceProfileSource, type DeviceProfileSourceDiagnostic, } from "./device-profile-source.js"; +import { LibraryDeviceProfileSource } from "./profiles/index.js"; const avdmanager = "/android-sdk/cmdline-tools/latest/bin/avdmanager"; const pixelDevices = `Available devices:\nid: 0 or "pixel_8"\n Name: Pixel 8\n OEM : Google\n`; @@ -869,6 +871,74 @@ describe("device classes", () => { expect(classes).toEqual({ "My Custom Phone": "phone", "Pixel 8": "phone" }); }); + it("classes a listed model by the profile its name resolves to, not the one it was listed from", async () => { + // why: the three devices.xml profiles are named after SDK profile ids -- two the library + // also ships, one (`tv_720p`) only the SDK has -- so a lease of each name creates the SDK's + // TV or Wear OS device; the class the catalog lists must follow that profile, not the + // untagged devices.xml entry the name was listed from. + const collidingDevicesXml = ` + tv_1080p2048 + tv_720p + wearos_large_round + My Custom Phone + `; + const sdkTelevision = + 'id: 58 or "tv_1080p"\n Name: Television (1080p)\n OEM : Google\n Tag : android-tv\n'; + const sdk720 = + 'id: 60 or "tv_720p"\n Name: Television (720p)\n OEM : Google\n Tag : android-tv\n'; + const sdkWear = + 'id: 61 or "wearos_large_round"\n Name: Wear OS Large Round\n OEM : Google\n Tag : android-wear\n'; + const sdkOutput = + `Available devices:\n${sdkTelevision}---------\n${sdk720}---------\n${sdkWear}---------\n` + + `id: 43 or "pixel_8"\n Name: Pixel 8\n OEM : Google\n---------\n`; + // why: `avdmanager list device` is read once for the catalog and once more for each resolve + // of a name the library lacks (`Television (720p)`, `tv_720p`, `My Custom Phone`, and the + // `tv_720p` resolve below). + const runner = new ScriptedProcessRunner( + Array.from({ length: 5 }, () => processResult(sdkOutput)), + ); + const registry = new DeviceProfileRegistry([ + new LibraryDeviceProfileSource(), + new BuiltinDeviceProfileSource(avdmanager, runner), + new UserDeviceProfileSource( + devicesXmlPath, + await filesystemWithDevicesXml(collidingDevicesXml), + ), + ]); + + const catalog = await registry.catalog(); + const expected: Record = {}; + for (const model of catalog.models) { + const resolved = await registry.resolve(model); + expected[model] = + resolved.kind === "properties" + ? resolved.imageKind === "android-tv" + ? "tv" + : resolved.imageKind === "android-wear" + ? "watch" + : resolved.imageKind === undefined + ? "phone" + : undefined + : resolved.deviceClass; + } + expect(catalog.modelClasses).toEqual(expected); + await expect(registry.resolve("tv_1080p")).resolves.toMatchObject({ + deviceClass: "tv", + kind: "library", + name: "Television (1080p)", + }); + await expect(registry.resolve("tv_720p")).resolves.toMatchObject({ + avdmanagerId: "tv_720p", + deviceClass: "tv", + kind: "builtin", + }); + await expect(registry.resolve("wearos_large_round")).resolves.toMatchObject({ + deviceClass: "watch", + kind: "library", + name: "Wear OS Large Round", + }); + }); + it("lists no class for a profile whose tag is outside the table, and keeps the model", async () => { const output = entry("odd", "Odd Device", "android-toaster") + entry("pixel_8", "Pixel 8"); const registry = new DeviceProfileRegistry([ From 2624fc655b8aed3d0b442383ba0f52ad58773c8b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 22:04:45 +0200 Subject: [PATCH 2/5] fix(drivers/android): class each catalog model by the profile its name resolves to 1 failing -> 0 failing --- src/drivers/android/device-profile-source.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/drivers/android/device-profile-source.ts b/src/drivers/android/device-profile-source.ts index 64650d6b5..bfe71519e 100644 --- a/src/drivers/android/device-profile-source.ts +++ b/src/drivers/android/device-profile-source.ts @@ -131,11 +131,18 @@ export class DeviceProfileRegistry { const profile = matchProfile(profiles, model); return profile !== undefined && isCustom(profile); }); + // why: a lease of a name creates the profile that name resolves to, so its class in + // the catalog must come from that profile, not the one it happened to be listed from. const modelClasses: Record = {}; - for (const profile of listed.values()) { + for (const model of models) { + const profile = matchProfile(profiles, model); const deviceClass = - profile.kind === "properties" ? classOfTag(profile.imageKind) : profile.deviceClass; - if (deviceClass !== undefined) modelClasses[profile.name] = deviceClass; + profile === undefined + ? undefined + : profile.kind === "properties" + ? classOfTag(profile.imageKind) + : profile.deviceClass; + if (deviceClass !== undefined) modelClasses[model] = deviceClass; } const modelProfiles = new Map(); for (const model of models) { From 02d1f0b8ef678294c0b30c8eae7c1fa8aa056745 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 22:07:42 +0200 Subject: [PATCH 3/5] docs: state that a model's class follows the profile its name resolves to --- docs/CLI.md | 4 +++- src/drivers/android/AGENTS.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/CLI.md b/docs/CLI.md index 6cdad893f..d04e6e63b 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1626,7 +1626,9 @@ from the device profile's tag: `android-tv` is `tv`, `android-wear` is `watch`, `android-automotive` and its variants are `auto`, `android-desktop` is `desktop`, and a profile with no tag is `phone`. A custom profile found only in `devices.xml` takes its class from the tag Android Studio saved -for it, and is `phone` when it saved none. A profile Simlock ships carries its own class. A model whose tooling reports nothing usable has +for it, and is `phone` when it saved none. When a custom name is one an SDK or +Simlock-shipped profile already answers to, the class is that profile's, since a +lease of that name creates its device. A profile Simlock ships carries its own class. A model whose tooling reports nothing usable has no entry, and is still listed and leasable by name. `classDefaults` says, for a class, which model Simlock would create for it on diff --git a/src/drivers/android/AGENTS.md b/src/drivers/android/AGENTS.md index 43e1d4582..30ed1a90e 100644 --- a/src/drivers/android/AGENTS.md +++ b/src/drivers/android/AGENTS.md @@ -83,6 +83,8 @@ Profiles — library (`profiles/`), SDK (`avdmanager list device`), devices.xml `ANDROID_SDK_HOME` or home). First case-insensitive name match wins. A devices.xml name the library shadows → advisory `device-profile-hidden`. SDK class from `Tag`: none `phone`, `android-tv` `tv`, `android-wear` `watch`, `android-automotive*` `auto`, `android-desktop` `desktop`. +The catalog classes each model by the profile its name resolves to — the one a lease of that +name creates — not the one it was listed from. Fit — `fits(profile, image)`: API range, image kind (tag), Play Store, tablet. The catalog's `modelRuntimes` lists only API levels with a fitting image. From 2170e19a61f5a12134c48c40690f71df4208214d Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 22:12:34 +0200 Subject: [PATCH 4/5] test: state the resolving profile per colliding name in the #341 test's comment --- src/drivers/android/device-profile-source.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/drivers/android/device-profile-source.test.ts b/src/drivers/android/device-profile-source.test.ts index 85d2dab13..273874a75 100644 --- a/src/drivers/android/device-profile-source.test.ts +++ b/src/drivers/android/device-profile-source.test.ts @@ -872,10 +872,11 @@ describe("device classes", () => { }); it("classes a listed model by the profile its name resolves to, not the one it was listed from", async () => { - // why: the three devices.xml profiles are named after SDK profile ids -- two the library - // also ships, one (`tv_720p`) only the SDK has -- so a lease of each name creates the SDK's - // TV or Wear OS device; the class the catalog lists must follow that profile, not the - // untagged devices.xml entry the name was listed from. + // why: the three devices.xml profiles are named after profile ids the library or the SDK + // ships -- `tv_1080p` and `wearos_large_round` resolve to the library's TV and Wear OS + // profiles, `tv_720p` to an SDK-only one -- so a lease of each name creates that TV or + // Wear OS device; the class the catalog lists must follow the profile the name resolves + // to, not the untagged devices.xml entry the name was listed from. const collidingDevicesXml = ` tv_1080p2048 tv_720p From 415f3856766b418048b29b8fbec82efec7e4370b Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 16 Oct 2026 22:28:38 +0200 Subject: [PATCH 5/5] docs(drivers/android): state the classing rule inside the manual's line budget --- src/drivers/android/AGENTS.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/drivers/android/AGENTS.md b/src/drivers/android/AGENTS.md index 30ed1a90e..5d64ab70e 100644 --- a/src/drivers/android/AGENTS.md +++ b/src/drivers/android/AGENTS.md @@ -79,12 +79,10 @@ started, then `avdmanager delete avd`, frees the port. `listManaged`: `/*.avd` + `adb devices` + `ro.boot.qemu.avd_name` per serial; only root members count. Profiles — library (`profiles/`), SDK (`avdmanager list device`), devices.xml -(`$ANDROID_USER_HOME/devices.xml`, else `.android/devices.xml` under `ANDROID_PREFS_ROOT`, -`ANDROID_SDK_HOME` or home). First case-insensitive name match wins. A devices.xml name the -library shadows → advisory `device-profile-hidden`. SDK class from `Tag`: none `phone`, +(`$ANDROID_USER_HOME/devices.xml`, else `.android/devices.xml` under `ANDROID_PREFS_ROOT`, `ANDROID_SDK_HOME` +or home). First case-insensitive name match wins and classes the model by the profile its name resolves to. A +devices.xml name the library shadows → advisory `device-profile-hidden`. SDK class from `Tag`: none `phone`, `android-tv` `tv`, `android-wear` `watch`, `android-automotive*` `auto`, `android-desktop` `desktop`. -The catalog classes each model by the profile its name resolves to — the one a lease of that -name creates — not the one it was listed from. Fit — `fits(profile, image)`: API range, image kind (tag), Play Store, tablet. The catalog's `modelRuntimes` lists only API levels with a fitting image.