@@ -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,207 @@ 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' , 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' , 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' , 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+
494+ test ( 'boot --headless rejects non-Android selectors' , async ( ) => {
495+ const sessionStore = makeSessionStore ( ) ;
496+ let resolved = false ;
497+ const response = await handleSessionCommands ( {
498+ req : {
499+ token : 't' ,
500+ session : 'default' ,
501+ command : 'boot' ,
502+ positionals : [ ] ,
503+ flags : { platform : 'ios' , device : 'iPhone 17 Pro' , headless : true } ,
504+ } ,
505+ sessionName : 'default' ,
506+ logPath : path . join ( os . tmpdir ( ) , 'daemon.log' ) ,
507+ sessionStore,
508+ invoke : noopInvoke ,
509+ ensureReady : async ( ) => { } ,
510+ resolveTargetDevice : async ( ) => {
511+ resolved = true ;
512+ throw new Error ( 'unexpected resolve' ) ;
513+ } ,
514+ ensureAndroidEmulatorBoot : async ( ) => {
515+ throw new Error ( 'unexpected emulator launch' ) ;
516+ } ,
517+ } ) ;
518+
519+ assert . ok ( response ) ;
520+ assert . equal ( response ?. ok , false ) ;
521+ assert . equal ( resolved , false ) ;
522+ if ( response && ! response . ok ) {
523+ assert . equal ( response . error . code , 'INVALID_ARGS' ) ;
524+ assert . match ( response . error . message , / h e a d l e s s i s s u p p o r t e d o n l y f o r A n d r o i d e m u l a t o r s / i) ;
525+ }
526+ } ) ;
527+
528+ test ( 'boot keeps --target validation when emulator is fallback-launched' , async ( ) => {
529+ const sessionStore = makeSessionStore ( ) ;
530+ let ensured = false ;
531+ const launchCalls : Array < { avdName : string ; serial ?: string ; headless ?: boolean } > = [ ] ;
532+ const response = await handleSessionCommands ( {
533+ req : {
534+ token : 't' ,
535+ session : 'default' ,
536+ command : 'boot' ,
537+ positionals : [ ] ,
538+ flags : { platform : 'android' , target : 'tv' , device : 'Pixel_9_Pro_XL' } ,
539+ } ,
540+ sessionName : 'default' ,
541+ logPath : path . join ( os . tmpdir ( ) , 'daemon.log' ) ,
542+ sessionStore,
543+ invoke : noopInvoke ,
544+ ensureReady : async ( ) => {
545+ ensured = true ;
546+ } ,
547+ resolveTargetDevice : async ( ) => {
548+ throw new AppError ( 'DEVICE_NOT_FOUND' , 'No Android TV devices found' ) ;
549+ } ,
550+ ensureAndroidEmulatorBoot : async ( { avdName, serial, headless } ) => {
551+ launchCalls . push ( { avdName, serial, headless } ) ;
552+ return {
553+ platform : 'android' ,
554+ id : 'emulator-5554' ,
555+ name : 'Pixel_9_Pro_XL' ,
556+ kind : 'emulator' ,
557+ target : 'mobile' ,
558+ booted : true ,
559+ } ;
560+ } ,
561+ } ) ;
562+
563+ assert . ok ( response ) ;
564+ assert . equal ( response ?. ok , false ) ;
565+ assert . equal ( ensured , false ) ;
566+ assert . deepEqual ( launchCalls , [ { avdName : 'Pixel_9_Pro_XL' , serial : undefined , headless : false } ] ) ;
567+ if ( response && ! response . ok ) {
568+ assert . equal ( response . error . code , 'DEVICE_NOT_FOUND' ) ;
569+ assert . match ( response . error . message , / m a t c h i n g - - t a r g e t t v / i) ;
570+ }
571+ } ) ;
572+
371573test ( 'appstate on iOS requires active session on selected device' , async ( ) => {
372574 const sessionStore = makeSessionStore ( ) ;
373575 const sessionName = 'default' ;
0 commit comments