@@ -3,13 +3,16 @@ import assert from 'node:assert/strict';
33import { promises as fs } from 'node:fs' ;
44import os from 'node:os' ;
55import path from 'node:path' ;
6+ import { AppError } from '../../../utils/errors.ts' ;
67import {
78 ensureAndroidEmulatorBooted ,
9+ listAndroidDevices ,
810 parseAndroidAvdList ,
911 parseAndroidEmulatorAvdNameOutput ,
1012 parseAndroidFeatureListForTv ,
1113 parseAndroidTargetFromCharacteristics ,
1214 resolveAndroidAvdName ,
15+ resolveAndroidEmulatorAvdName ,
1316} from '../devices.ts' ;
1417
1518const MOCK_ANDROID_ADB_SCRIPT = [
@@ -22,14 +25,23 @@ const MOCK_ANDROID_ADB_SCRIPT = [
2225 ' exit 0' ,
2326 'fi' ,
2427 'if [ "$1" = "-s" ] && [ "$2" = "emulator-5554" ] && [ "$3" = "emu" ] && [ "$4" = "avd" ] && [ "$5" = "name" ]; then' ,
28+ ' if [ "$AGENT_DEVICE_TEST_AVD_NAME_MODE" = "missing" ]; then' ,
29+ ' exit 0' ,
30+ ' fi' ,
2531 ' echo "Pixel_9_Pro_XL"' ,
2632 ' exit 0' ,
2733 'fi' ,
2834 'if [ "$1" = "-s" ] && [ "$2" = "emulator-5554" ] && [ "$3" = "shell" ] && [ "$4" = "getprop" ] && [ "$5" = "ro.boot.qemu.avd_name" ]; then' ,
35+ ' if [ "$AGENT_DEVICE_TEST_AVD_NAME_MODE" = "missing" ]; then' ,
36+ ' exit 0' ,
37+ ' fi' ,
2938 ' echo "Pixel_9_Pro_XL"' ,
3039 ' exit 0' ,
3140 'fi' ,
3241 'if [ "$1" = "-s" ] && [ "$2" = "emulator-5554" ] && [ "$3" = "shell" ] && [ "$4" = "getprop" ] && [ "$5" = "persist.sys.avd_name" ]; then' ,
42+ ' if [ "$AGENT_DEVICE_TEST_AVD_NAME_MODE" = "missing" ]; then' ,
43+ ' exit 0' ,
44+ ' fi' ,
3345 ' echo "Pixel_9_Pro_XL"' ,
3446 ' exit 0' ,
3547 'fi' ,
@@ -137,6 +149,7 @@ test('resolveAndroidAvdName supports space vs underscore matching', () => {
137149
138150async function withMockedAndroidTools (
139151 run : ( ctx : { emulatorLogPath : string ; emulatorBootedPath : string } ) => Promise < void > ,
152+ options : { avdNameMode ?: 'success' | 'missing' } = { } ,
140153) : Promise < void > {
141154 const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'agent-device-android-headless-' ) ) ;
142155 const emulatorLogPath = path . join ( tmpDir , 'emulator.log' ) ;
@@ -153,6 +166,7 @@ async function withMockedAndroidTools(
153166 PATH : `${ tmpDir } ${ path . delimiter } ${ process . env . PATH ?? '' } ` ,
154167 AGENT_DEVICE_TEST_EMU_BOOTED_FILE : emulatorBootedPath ,
155168 AGENT_DEVICE_TEST_EMU_LOG_FILE : emulatorLogPath ,
169+ AGENT_DEVICE_TEST_AVD_NAME_MODE : options . avdNameMode ?? 'success' ,
156170 HOME : tmpDir ,
157171 ANDROID_SDK_ROOT : undefined ,
158172 ANDROID_HOME : undefined ,
@@ -198,6 +212,72 @@ async function withMockedAndroidSdkRoot(
198212 }
199213}
200214
215+ test ( 'resolveAndroidEmulatorAvdName ignores probe timeouts and keeps probing' , async ( ) => {
216+ const calls : string [ ] [ ] = [ ] ;
217+ const results = [
218+ new AppError ( 'COMMAND_FAILED' , 'adb timed out after 1500ms' , { timeoutMs : 1500 } ) ,
219+ { stdout : '' , stderr : '' , exitCode : 0 } ,
220+ { stdout : 'Pixel_9_Pro_XL\n' , stderr : '' , exitCode : 0 } ,
221+ ] ;
222+ const runAdb = async (
223+ _cmd : string ,
224+ args : string [ ] ,
225+ ) : Promise < { stdout : string ; stderr : string ; exitCode : number } > => {
226+ calls . push ( args ) ;
227+ const next = results . shift ( ) ;
228+ if ( next instanceof AppError ) throw next ;
229+ assert . ok ( next ) ;
230+ return next ;
231+ } ;
232+
233+ const avdName = await resolveAndroidEmulatorAvdName ( 'emulator-5554' , runAdb ) ;
234+
235+ assert . equal ( avdName , 'Pixel_9_Pro_XL' ) ;
236+ assert . deepEqual (
237+ calls . map ( ( args ) => args . slice ( 2 ) ) ,
238+ [
239+ [ 'shell' , 'getprop' , 'ro.boot.qemu.avd_name' ] ,
240+ [ 'shell' , 'getprop' , 'persist.sys.avd_name' ] ,
241+ [ 'emu' , 'avd' , 'name' ] ,
242+ ] ,
243+ ) ;
244+ } ) ;
245+
246+ test ( 'resolveAndroidEmulatorAvdName rethrows non-timeout probe failures' , async ( ) => {
247+ const failure = new AppError ( 'COMMAND_FAILED' , 'adb exited with code 1' , {
248+ stderr : 'device offline' ,
249+ exitCode : 1 ,
250+ processExitError : true ,
251+ } ) ;
252+ let callCount = 0 ;
253+ const runAdb = async ( ) : Promise < { stdout : string ; stderr : string ; exitCode : number } > => {
254+ callCount += 1 ;
255+ throw failure ;
256+ } ;
257+
258+ await assert . rejects (
259+ async ( ) => await resolveAndroidEmulatorAvdName ( 'emulator-5554' , runAdb ) ,
260+ ( error ) => error === failure ,
261+ ) ;
262+ assert . equal ( callCount , 1 ) ;
263+ } ) ;
264+
265+ test ( 'listAndroidDevices falls back to model when emulator avd name is unavailable' , async ( ) => {
266+ await withMockedAndroidTools (
267+ async ( { emulatorBootedPath } ) => {
268+ await fs . writeFile ( emulatorBootedPath , 'ready' , 'utf8' ) ;
269+
270+ const devices = await listAndroidDevices ( ) ;
271+
272+ assert . equal ( devices . length , 1 ) ;
273+ assert . equal ( devices [ 0 ] ?. id , 'emulator-5554' ) ;
274+ assert . equal ( devices [ 0 ] ?. name , 'Pixel 9 Pro XL' ) ;
275+ assert . equal ( devices [ 0 ] ?. kind , 'emulator' ) ;
276+ } ,
277+ { avdNameMode : 'missing' } ,
278+ ) ;
279+ } ) ;
280+
201281test ( 'ensureAndroidEmulatorBooted launches emulator in headless mode when requested' , async ( ) => {
202282 await withMockedAndroidTools ( async ( { emulatorLogPath, emulatorBootedPath } ) => {
203283 const device = await ensureAndroidEmulatorBooted ( {
0 commit comments