@@ -32,29 +32,42 @@ type MockClient = {
3232 brokerPid ?: number
3333 baseUrl ?: string
3434 agentNames : string [ ]
35+ agentRuntimes : Record < string , 'pty' | 'headless' >
3536}
3637
3738const mock = vi . hoisted ( ( ) => {
3839 function createMockClient ( agentNames : string [ ] = [ ] ) : MockClient {
40+ const agentRuntimes = Object . fromEntries ( agentNames . map ( ( name ) => [ name , 'pty' as const ] ) )
3941 const client : MockClient = {
4042 agentNames : [ ...agentNames ] ,
43+ agentRuntimes,
4144 getSession : vi . fn ( async ( ) => ( { } ) ) ,
42- listAgents : vi . fn ( async ( ) => client . agentNames . map ( ( name ) => ( { name, runtime : 'pty' , channels : [ ] } ) ) ) ,
45+ listAgents : vi . fn ( async ( ) => client . agentNames . map ( ( name ) => ( {
46+ name,
47+ runtime : client . agentRuntimes [ name ] || 'pty' ,
48+ channels : [ ]
49+ } ) ) ) ,
4350 getInboundDeliveryMode : vi . fn ( async ( ) => 'passthrough' ) ,
4451 spawnPty : vi . fn ( async ( input : { name : string } ) => {
4552 client . agentNames . push ( input . name )
53+ client . agentRuntimes [ input . name ] = 'pty'
4654 return { name : input . name , runtime : 'pty' }
4755 } ) ,
4856 spawnCli : vi . fn ( async ( input : { name : string } ) => {
4957 client . agentNames . push ( input . name )
58+ client . agentRuntimes [ input . name ] = 'headless'
5059 return { name : input . name , runtime : 'headless' }
5160 } ) ,
5261 setInboundDeliveryMode : vi . fn ( async ( _name : string , mode : string ) => ( { mode, flushed : 0 } ) ) ,
5362 snapshot : vi . fn ( async ( ) => ( { rows : 24 , cols : 80 , cursor : { x : 0 , y : 0 } , screen : 'aGVsbG8=' } ) ) ,
5463 resizePty : vi . fn ( async ( ) => undefined ) ,
5564 getPending : vi . fn ( async ( ) => [ ] ) ,
5665 getStatus : vi . fn ( async ( ) => ( {
57- agents : client . agentNames . map ( ( name ) => ( { name, runtime : 'pty' , channels : [ ] } ) ) ,
66+ agents : client . agentNames . map ( ( name ) => ( {
67+ name,
68+ runtime : client . agentRuntimes [ name ] || 'pty' ,
69+ channels : [ ]
70+ } ) ) ,
5871 pending_delivery_count : 0
5972 } ) ) ,
6073 onEvent : vi . fn ( ( ) => ( ) => undefined ) ,
@@ -542,6 +555,60 @@ describe('BrokerManager local + cloud coexistence', () => {
542555 await manager . shutdown ( )
543556 } )
544557
558+ it ( 'spawns OpenCode with headless runtime and skips PTY attach operations' , async ( ) => {
559+ const manager = new BrokerManager ( )
560+ const local = await startLocal ( manager , [ ] )
561+
562+ const spawned = await manager . spawnAgent ( PROJECT_ID , { name : 'opencode-1' , cli : 'opencode' } )
563+ const attached = await manager . attachTerminal ( PROJECT_ID , {
564+ name : spawned . name ,
565+ mode : 'passthrough' ,
566+ rows : 24 ,
567+ cols : 80
568+ } )
569+
570+ expect ( spawned ) . toEqual ( { name : 'opencode-1' , runtime : 'headless' } )
571+ expect ( local . spawnCli ) . toHaveBeenCalledWith ( expect . objectContaining ( {
572+ name : 'opencode-1' ,
573+ cli : expect . stringContaining ( 'opencode' ) ,
574+ transport : 'headless'
575+ } ) )
576+ expect ( local . spawnPty ) . not . toHaveBeenCalled ( )
577+ expect ( local . resizePty ) . not . toHaveBeenCalled ( )
578+ expect ( local . snapshot ) . not . toHaveBeenCalled ( )
579+ expect ( attached ) . toEqual ( {
580+ name : 'opencode-1' ,
581+ mode : 'auto_inject' ,
582+ previousMode : 'passthrough' ,
583+ pending : 0 ,
584+ runtime : 'headless'
585+ } )
586+
587+ await manager . shutdown ( )
588+ } )
589+
590+ it ( 'remembers headless runtime from discovered agents before attaching' , async ( ) => {
591+ const manager = new BrokerManager ( )
592+ const local = await startLocal ( manager , [ ] )
593+ local . listAgents . mockResolvedValue ( [
594+ { name : 'opencode-1' , runtime : 'headless' , channels : [ ] }
595+ ] )
596+
597+ const attached = await manager . attachTerminal ( PROJECT_ID , {
598+ name : 'opencode-1' ,
599+ mode : 'passthrough' ,
600+ rows : 24 ,
601+ cols : 80
602+ } )
603+
604+ expect ( local . setInboundDeliveryMode ) . toHaveBeenCalledWith ( 'opencode-1' , 'auto_inject' )
605+ expect ( local . resizePty ) . not . toHaveBeenCalled ( )
606+ expect ( local . snapshot ) . not . toHaveBeenCalled ( )
607+ expect ( attached . runtime ) . toBe ( 'headless' )
608+
609+ await manager . shutdown ( )
610+ } )
611+
545612 it ( 'coalesces concurrent duplicate spawn requests' , async ( ) => {
546613 const manager = new BrokerManager ( )
547614 const local = await startLocal ( manager , [ ] )
0 commit comments