@@ -6,6 +6,7 @@ import path from 'node:path';
66import { handleSessionCommands } from '../session.ts' ;
77import { SessionStore } from '../../session-store.ts' ;
88import type { DaemonRequest , DaemonResponse , SessionState } from '../../types.ts' ;
9+ import { AppError } from '../../../utils/errors.ts' ;
910
1011function makeSessionStore ( ) : SessionStore {
1112 const root = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'agent-device-session-handler-' ) ) ;
@@ -312,7 +313,7 @@ test('boot succeeds for supported device in session', async () => {
312313 } ) ;
313314 assert . ok ( response ) ;
314315 assert . equal ( response ?. ok , true ) ;
315- assert . equal ( ensureCalls , 1 ) ;
316+ assert . equal ( ensureCalls , 0 ) ;
316317 if ( response && response . ok ) {
317318 assert . equal ( response . data ?. platform , 'android' ) ;
318319 assert . equal ( response . data ?. booted , true ) ;
@@ -368,6 +369,128 @@ test('boot prefers explicit device selector over active session device', async (
368369 }
369370} ) ;
370371
372+ test ( 'boot --headless launches Android emulator when no running device matches' , async ( ) => {
373+ const sessionStore = makeSessionStore ( ) ;
374+ const ensured : string [ ] = [ ] ;
375+ const launchCalls : Array < { avdName : string ; serial ?: string ; headless ?: boolean } > = [ ] ;
376+ const response = await handleSessionCommands ( {
377+ req : {
378+ token : 't' ,
379+ session : 'default' ,
380+ command : 'boot' ,
381+ positionals : [ ] ,
382+ flags : { platform : 'android' , target : 'mobile' , device : 'Pixel_9_Pro_XL' , headless : true } ,
383+ } ,
384+ sessionName : 'default' ,
385+ logPath : path . join ( os . tmpdir ( ) , 'daemon.log' ) ,
386+ sessionStore,
387+ invoke : noopInvoke ,
388+ ensureReady : async ( device ) => {
389+ ensured . push ( device . id ) ;
390+ } ,
391+ resolveTargetDevice : async ( ) => {
392+ throw new AppError ( 'DEVICE_NOT_FOUND' , 'No devices found' ) ;
393+ } ,
394+ ensureAndroidEmulatorBoot : async ( { avdName, serial, headless } ) => {
395+ launchCalls . push ( { avdName, serial, headless } ) ;
396+ return {
397+ platform : 'android' ,
398+ id : 'emulator-5554' ,
399+ name : 'Pixel_9_Pro_XL' ,
400+ kind : 'emulator' ,
401+ target : 'mobile' ,
402+ booted : true ,
403+ } ;
404+ } ,
405+ } ) ;
406+
407+ assert . ok ( response ) ;
408+ assert . equal ( response ?. ok , true ) ;
409+ assert . deepEqual ( launchCalls , [ { avdName : 'Pixel_9_Pro_XL' , serial : undefined , headless : true } ] ) ;
410+ assert . deepEqual ( ensured , [ 'emulator-5554' ] ) ;
411+ if ( response && response . ok ) {
412+ assert . equal ( response . data ?. platform , 'android' ) ;
413+ assert . equal ( response . data ?. id , 'emulator-5554' ) ;
414+ assert . equal ( response . data ?. device , 'Pixel_9_Pro_XL' ) ;
415+ }
416+ } ) ;
417+
418+ test ( 'boot launches Android emulator with GUI when no running device matches' , async ( ) => {
419+ const sessionStore = makeSessionStore ( ) ;
420+ const launchCalls : Array < { avdName : string ; serial ?: string ; headless ?: boolean } > = [ ] ;
421+ const response = await handleSessionCommands ( {
422+ req : {
423+ token : 't' ,
424+ session : 'default' ,
425+ command : 'boot' ,
426+ positionals : [ ] ,
427+ flags : { platform : 'android' , target : 'mobile' , device : 'Pixel_9_Pro_XL' } ,
428+ } ,
429+ sessionName : 'default' ,
430+ logPath : path . join ( os . tmpdir ( ) , 'daemon.log' ) ,
431+ sessionStore,
432+ invoke : noopInvoke ,
433+ ensureReady : async ( ) => { } ,
434+ resolveTargetDevice : async ( ) => {
435+ throw new AppError ( 'DEVICE_NOT_FOUND' , 'No devices found' ) ;
436+ } ,
437+ ensureAndroidEmulatorBoot : async ( { avdName, serial, headless } ) => {
438+ launchCalls . push ( { avdName, serial, headless } ) ;
439+ return {
440+ platform : 'android' ,
441+ id : 'emulator-5554' ,
442+ name : 'Pixel_9_Pro_XL' ,
443+ kind : 'emulator' ,
444+ target : 'mobile' ,
445+ booted : true ,
446+ } ;
447+ } ,
448+ } ) ;
449+
450+ assert . ok ( response ) ;
451+ assert . equal ( response ?. ok , true ) ;
452+ assert . deepEqual ( launchCalls , [ { avdName : 'Pixel_9_Pro_XL' , serial : undefined , headless : false } ] ) ;
453+ if ( response && response . ok ) {
454+ assert . equal ( response . data ?. platform , 'android' ) ;
455+ assert . equal ( response . data ?. id , 'emulator-5554' ) ;
456+ assert . equal ( response . data ?. device , 'Pixel_9_Pro_XL' ) ;
457+ }
458+ } ) ;
459+
460+ test ( 'boot --headless requires avd selector when device cannot be resolved' , async ( ) => {
461+ const sessionStore = makeSessionStore ( ) ;
462+ let bootCalled = false ;
463+ const response = await handleSessionCommands ( {
464+ req : {
465+ token : 't' ,
466+ session : 'default' ,
467+ command : 'boot' ,
468+ positionals : [ ] ,
469+ flags : { platform : 'android' , target : 'mobile' , serial : 'emulator-5554' , headless : true } ,
470+ } ,
471+ sessionName : 'default' ,
472+ logPath : path . join ( os . tmpdir ( ) , 'daemon.log' ) ,
473+ sessionStore,
474+ invoke : noopInvoke ,
475+ ensureReady : async ( ) => { } ,
476+ resolveTargetDevice : async ( ) => {
477+ throw new AppError ( 'DEVICE_NOT_FOUND' , 'No devices found' ) ;
478+ } ,
479+ ensureAndroidEmulatorBoot : async ( ) => {
480+ bootCalled = true ;
481+ throw new Error ( 'unexpected' ) ;
482+ } ,
483+ } ) ;
484+
485+ assert . ok ( response ) ;
486+ assert . equal ( response ?. ok , false ) ;
487+ assert . equal ( bootCalled , false ) ;
488+ if ( response && ! response . ok ) {
489+ assert . equal ( response . error . code , 'INVALID_ARGS' ) ;
490+ assert . match ( response . error . message , / b o o t - - h e a d l e s s r e q u i r e s - - d e v i c e < a v d - n a m e > / ) ;
491+ }
492+ } ) ;
493+
371494test ( 'appstate on iOS requires active session on selected device' , async ( ) => {
372495 const sessionStore = makeSessionStore ( ) ;
373496 const sessionName = 'default' ;
0 commit comments