2026-02-21 05:59:30 +09:00
import { describe , test , expect , mock , beforeEach , spyOn } from 'bun:test'
2026-01-25 15:34:10 +09:00
import type { TmuxConfig } from '../../config/schema'
2026-01-26 12:02:37 +09:00
import type { WindowState , PaneAction } from './types'
2026-01-26 15:11:16 +09:00
import type { ActionResult , ExecuteContext } from './action-executor'
2026-01-30 10:59:54 +09:00
import type { TmuxUtilDeps } from './manager'
2026-02-21 05:59:30 +09:00
import * as sharedModule from '../../shared'
2026-01-25 15:34:10 +09:00
2026-01-26 12:02:37 +09:00
type ExecuteActionsResult = {
success : boolean
spawnedPaneId? : string
results : Array < { action : PaneAction ; result : ActionResult } >
}
2026-03-31 16:58:45 -07:00
type SpawnTmuxContainerResult = {
success : boolean
paneId? : string
}
2026-01-26 12:02:37 +09:00
const mockQueryWindowState = mock < ( paneId : string ) = > Promise < WindowState | null > > (
async ( ) = > ( {
2026-01-26 15:11:16 +09:00
windowWidth : 212 ,
windowHeight : 44 ,
mainPane : { paneId : '%0' , width : 106 , height : 44 , left : 0 , top : 0 , title : 'main' , isActive : true } ,
2026-01-26 12:02:37 +09:00
agentPanes : [ ] ,
} )
)
const mockPaneExists = mock < ( paneId : string ) = > Promise < boolean > > ( async ( ) = > true )
const mockExecuteActions = mock < (
actions : PaneAction [ ] ,
2026-01-26 15:11:16 +09:00
ctx : ExecuteContext
2026-01-26 12:02:37 +09:00
) = > Promise < ExecuteActionsResult > > ( async ( ) = > ( {
success : true ,
spawnedPaneId : '%mock' ,
results : [ ] ,
} ) )
const mockExecuteAction = mock < (
action : PaneAction ,
2026-01-26 15:11:16 +09:00
ctx : ExecuteContext
2026-01-26 12:02:37 +09:00
) = > Promise < ActionResult > > ( async ( ) = > ( { success : true } ) )
2026-03-31 16:58:45 -07:00
const mockSpawnTmuxWindow = mock < (
sessionId : string ,
description : string ,
config : TmuxConfig ,
serverUrl : string
) = > Promise < SpawnTmuxContainerResult > > ( async ( ) = > ( {
success : true ,
paneId : '%isolated-window' ,
} ) )
const mockSpawnTmuxSession = mock < (
sessionId : string ,
description : string ,
config : TmuxConfig ,
serverUrl : string ,
sourcePaneId? : string
) = > Promise < SpawnTmuxContainerResult > > ( async ( ) = > ( {
success : true ,
paneId : '%isolated-session' ,
} ) )
2026-01-26 12:02:37 +09:00
const mockIsInsideTmux = mock < ( ) = > boolean > ( ( ) = > true )
const mockGetCurrentPaneId = mock < ( ) = > string | undefined > ( ( ) = > '%0' )
2026-01-30 10:59:54 +09:00
const mockTmuxDeps : TmuxUtilDeps = {
isInsideTmux : mockIsInsideTmux ,
getCurrentPaneId : mockGetCurrentPaneId ,
}
2026-01-26 12:02:37 +09:00
mock . module ( './pane-state-querier' , ( ) = > ( {
queryWindowState : mockQueryWindowState ,
paneExists : mockPaneExists ,
getRightmostAgentPane : ( state : WindowState ) = >
state . agentPanes . length > 0
? state . agentPanes . reduce ( ( r , p ) = > ( p . left > r . left ? p : r ) )
: null ,
getOldestAgentPane : ( state : WindowState ) = >
state . agentPanes . length > 0
? state . agentPanes . reduce ( ( o , p ) = > ( p . left < o . left ? p : o ) )
: null ,
} ) )
mock . module ( './action-executor' , ( ) = > ( {
executeActions : mockExecuteActions ,
executeAction : mockExecuteAction ,
2026-02-17 01:36:01 +09:00
executeActionWithDeps : mockExecuteAction ,
2026-01-26 12:02:37 +09:00
} ) )
2026-01-25 15:34:10 +09:00
2026-01-30 10:59:54 +09:00
mock . module ( '../../shared/tmux' , ( ) = > {
const { isInsideTmux , getCurrentPaneId } = require ( '../../shared/tmux/tmux-utils' )
const { POLL_INTERVAL_BACKGROUND_MS , SESSION_TIMEOUT_MS , SESSION_MISSING_GRACE_MS } = require ( '../../shared/tmux/constants' )
return {
isInsideTmux ,
getCurrentPaneId ,
POLL_INTERVAL_BACKGROUND_MS ,
SESSION_TIMEOUT_MS ,
SESSION_MISSING_GRACE_MS ,
SESSION_READY_POLL_INTERVAL_MS : 100 ,
SESSION_READY_TIMEOUT_MS : 500 ,
2026-03-31 16:58:45 -07:00
spawnTmuxWindow : mockSpawnTmuxWindow ,
spawnTmuxSession : mockSpawnTmuxSession ,
2026-01-30 10:59:54 +09:00
}
} )
2026-01-25 15:34:10 +09:00
2026-01-26 12:02:37 +09:00
const trackedSessions = new Set < string > ( )
2026-01-25 15:34:10 +09:00
function createMockContext ( overrides ? : {
sessionStatusResult ? : { data? : Record < string , { type : string } > }
2026-02-01 11:11:35 +01:00
sessionMessagesResult ? : { data? : unknown [ ] }
2026-01-25 15:34:10 +09:00
} ) {
return {
serverUrl : new URL ( 'http://localhost:4096' ) ,
client : {
session : {
2026-01-26 12:02:37 +09:00
status : mock ( async ( ) = > {
if ( overrides ? . sessionStatusResult ) {
return overrides . sessionStatusResult
}
const data : Record < string , { type : string } > = { }
for ( const sessionId of trackedSessions ) {
data [ sessionId ] = { type : 'running' }
}
return { data }
} ) ,
2026-02-01 11:11:35 +01:00
messages : mock ( async ( ) = > {
if ( overrides ? . sessionMessagesResult ) {
return overrides . sessionMessagesResult
}
return { data : [ ] }
} ) ,
2026-01-25 15:34:10 +09:00
} ,
} ,
} as any
}
2026-01-26 12:02:37 +09:00
function createSessionCreatedEvent (
id : string ,
parentID : string | undefined ,
title : string
) {
return {
type : 'session.created' ,
properties : {
info : { id , parentID , title } ,
} ,
}
}
function createWindowState ( overrides? : Partial < WindowState > ) : WindowState {
return {
2026-01-26 15:25:05 +09:00
windowWidth : 220 ,
2026-01-26 15:11:16 +09:00
windowHeight : 44 ,
2026-01-26 15:25:05 +09:00
mainPane : { paneId : '%0' , width : 110 , height : 44 , left : 0 , top : 0 , title : 'main' , isActive : true } ,
2026-01-26 12:02:37 +09:00
agentPanes : [ ] ,
. . . overrides ,
}
}
2026-01-25 15:34:10 +09:00
describe ( 'TmuxSessionManager' , ( ) = > {
beforeEach ( ( ) = > {
2026-01-26 12:02:37 +09:00
mockQueryWindowState . mockClear ( )
mockPaneExists . mockClear ( )
mockExecuteActions . mockClear ( )
mockExecuteAction . mockClear ( )
2026-03-31 16:58:45 -07:00
mockSpawnTmuxWindow . mockClear ( )
mockSpawnTmuxSession . mockClear ( )
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockClear ( )
2026-01-26 12:02:37 +09:00
mockGetCurrentPaneId . mockClear ( )
trackedSessions . clear ( )
mockQueryWindowState . mockImplementation ( async ( ) = > createWindowState ( ) )
mockExecuteActions . mockImplementation ( async ( actions ) = > {
for ( const action of actions ) {
if ( action . type === 'spawn' ) {
trackedSessions . add ( action . sessionId )
}
}
return {
success : true ,
spawnedPaneId : '%mock' ,
results : [ ] ,
}
} )
2026-03-31 16:58:45 -07:00
mockSpawnTmuxWindow . mockImplementation ( async ( sessionId ) = > {
trackedSessions . add ( sessionId )
return {
success : true ,
paneId : ` %isolated-window- ${ sessionId } ` ,
}
} )
mockSpawnTmuxSession . mockImplementation ( async ( sessionId ) = > {
trackedSessions . add ( sessionId )
return {
success : true ,
paneId : ` %isolated-session- ${ sessionId } ` ,
}
} )
2026-01-25 15:34:10 +09:00
} )
describe ( 'constructor' , ( ) = > {
test ( 'enabled when config.enabled=true and isInsideTmux=true' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
2026-02-15 03:26:39 +08:00
const ctx = createMockContext ( {
sessionStatusResult : {
data : {
ses_1 : { type : 'running' } ,
ses_2 : { type : 'running' } ,
ses_3 : { type : 'running' } ,
} ,
} ,
} )
2026-01-25 15:34:10 +09:00
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-02-01 16:47:50 +09:00
// when
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-25 15:34:10 +09:00
expect ( manager ) . toBeDefined ( )
} )
test ( 'disabled when config.enabled=true but isInsideTmux=false' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( false )
const { TmuxSessionManager } = await import ( './manager' )
2026-02-15 03:26:39 +08:00
const ctx = createMockContext ( {
sessionStatusResult : {
data : {
ses_once : { type : 'running' } ,
} ,
} ,
} )
2026-01-25 15:34:10 +09:00
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-02-01 16:47:50 +09:00
// when
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-25 15:34:10 +09:00
expect ( manager ) . toBeDefined ( )
} )
test ( 'disabled when config.enabled=false' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : false ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-02-01 16:47:50 +09:00
// when
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-25 15:34:10 +09:00
expect ( manager ) . toBeDefined ( )
} )
2026-03-23 18:09:31 +09:00
test ( 'falls back to default port when serverUrl has port 0' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = {
. . . createMockContext ( ) ,
serverUrl : new URL ( 'http://127.0.0.1:0/' ) ,
}
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
// when
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
// then
expect ( ( manager as any ) . serverUrl ) . toBe ( 'http://localhost:4096' )
} )
2026-01-25 15:34:10 +09:00
} )
describe ( 'onSessionCreated' , ( ) = > {
2026-01-26 12:02:37 +09:00
test ( 'first agent spawns from source pane via decision engine' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
2026-01-26 12:02:37 +09:00
mockQueryWindowState . mockImplementation ( async ( ) = > createWindowState ( ) )
2026-01-25 15:34:10 +09:00
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-26 12:02:37 +09:00
const event = createSessionCreatedEvent (
'ses_child' ,
'ses_parent' ,
'Background: Test Task'
)
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated ( event )
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockQueryWindowState ) . toHaveBeenCalledTimes ( 1 )
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 1 )
const call = mockExecuteActions . mock . calls [ 0 ]
expect ( call ) . toBeDefined ( )
const actionsArg = call ! [ 0 ]
expect ( actionsArg ) . toHaveLength ( 1 )
2026-01-26 15:11:16 +09:00
expect ( actionsArg [ 0 ] . type ) . toBe ( 'spawn' )
if ( actionsArg [ 0 ] . type === 'spawn' ) {
expect ( actionsArg [ 0 ] . sessionId ) . toBe ( 'ses_child' )
expect ( actionsArg [ 0 ] . description ) . toBe ( 'Background: Test Task' )
expect ( actionsArg [ 0 ] . targetPaneId ) . toBe ( '%0' )
expect ( actionsArg [ 0 ] . splitDirection ) . toBe ( '-h' )
}
2026-01-26 12:02:37 +09:00
} )
2026-01-26 15:11:16 +09:00
test ( 'second agent spawns with correct split direction' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-26 12:02:37 +09:00
mockIsInsideTmux . mockReturnValue ( true )
let callCount = 0
mockQueryWindowState . mockImplementation ( async ( ) = > {
callCount ++
if ( callCount === 1 ) {
return createWindowState ( )
}
return createWindowState ( {
agentPanes : [
{
paneId : '%1' ,
width : 40 ,
2026-01-26 15:11:16 +09:00
height : 44 ,
left : 100 ,
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'omo-subagent-Task 1' ,
isActive : false ,
} ,
] ,
} )
} )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when - first agent
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_1' , 'ses_parent' , 'Task 1' )
)
mockExecuteActions . mockClear ( )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when - second agent
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_2' , 'ses_parent' , 'Task 2' )
2026-01-25 15:34:10 +09:00
)
2026-01-26 12:02:37 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 1 )
const call = mockExecuteActions . mock . calls [ 0 ]
expect ( call ) . toBeDefined ( )
const actionsArg = call ! [ 0 ]
expect ( actionsArg ) . toHaveLength ( 1 )
2026-01-26 15:11:16 +09:00
expect ( actionsArg [ 0 ] . type ) . toBe ( 'spawn' )
2026-01-25 15:34:10 +09:00
} )
2026-03-31 16:58:45 -07:00
test ( '#given session isolation with healthy existing container #when second subagent is created #then it spawns inline from isolated pane' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( paneId ) = > {
if ( paneId === '%isolated-session-ses_first' ) {
return createWindowState ( {
mainPane : {
paneId ,
width : 110 ,
height : 44 ,
left : 0 ,
top : 0 ,
title : 'isolated' ,
isActive : true ,
} ,
} )
}
return createWindowState ( )
} )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
isolation : 'session' ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_first' , 'ses_parent' , 'First Task' )
)
mockExecuteActions . mockClear ( )
// when
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_second' , 'ses_parent' , 'Second Task' )
)
// then
expect ( mockSpawnTmuxSession ) . toHaveBeenCalledTimes ( 1 )
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 1 )
const executeActionsCall = mockExecuteActions . mock . calls [ 0 ]
expect ( executeActionsCall ) . toBeDefined ( )
const actions = executeActionsCall ? . [ 0 ]
const context = executeActionsCall ? . [ 1 ]
expect ( actions ) . toBeDefined ( )
expect ( actions ) . toHaveLength ( 1 )
expect ( actions ? . [ 0 ] ? . type ) . toBe ( 'spawn' )
if ( actions ? . [ 0 ] ? . type === 'spawn' ) {
expect ( actions [ 0 ] . sessionId ) . toBe ( 'ses_second' )
expect ( actions [ 0 ] . targetPaneId ) . toBe ( '%isolated-session-ses_first' )
}
expect ( context ? . sourcePaneId ) . toBe ( '%isolated-session-ses_first' )
} )
2026-01-25 15:34:10 +09:00
test ( 'does NOT spawn pane when session has no parentID' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-26 12:02:37 +09:00
const event = createSessionCreatedEvent ( 'ses_root' , undefined , 'Root Session' )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when
2026-01-25 15:34:10 +09:00
await manager . onSessionCreated ( event )
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 0 )
2026-01-25 15:34:10 +09:00
} )
test ( 'does NOT spawn pane when disabled' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : false ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-26 12:02:37 +09:00
const event = createSessionCreatedEvent (
'ses_child' ,
'ses_parent' ,
'Background: Test Task'
)
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when
2026-01-25 15:34:10 +09:00
await manager . onSessionCreated ( event )
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 0 )
2026-01-25 15:34:10 +09:00
} )
2026-01-26 12:02:37 +09:00
test ( 'does NOT spawn pane for non session.created event type' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-26 12:02:37 +09:00
const event = {
type : 'session.deleted' ,
properties : {
info : { id : 'ses_child' , parentID : 'ses_parent' , title : 'Task' } ,
} ,
}
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated ( event )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 0 )
2026-01-25 15:34:10 +09:00
} )
2026-02-15 03:26:39 +08:00
test ( 'defers attach when unsplittable (small window)' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given - small window where split is not possible
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
2026-01-26 12:02:37 +09:00
mockQueryWindowState . mockImplementation ( async ( ) = >
createWindowState ( {
windowWidth : 160 ,
2026-01-26 15:11:16 +09:00
windowHeight : 11 ,
2026-01-26 12:02:37 +09:00
agentPanes : [
{
paneId : '%1' ,
width : 40 ,
2026-01-26 15:11:16 +09:00
height : 11 ,
left : 80 ,
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'omo-subagent-Task 1' ,
isActive : false ,
} ,
] ,
} )
)
2026-01-25 15:34:10 +09:00
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 120 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_new' , 'ses_parent' , 'New Task' )
)
2026-02-15 03:26:39 +08:00
// then - with small window, manager defers instead of replacing
expect ( mockExecuteActions ) . toHaveBeenCalledTimes ( 0 )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_new' ] )
} )
test ( 'keeps deferred queue idempotent for duplicate session.created events' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = >
createWindowState ( {
windowWidth : 160 ,
windowHeight : 11 ,
agentPanes : [
{
paneId : '%1' ,
width : 80 ,
height : 11 ,
left : 80 ,
top : 0 ,
title : 'old' ,
isActive : false ,
} ,
] ,
} )
)
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 120 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
// when
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_dup' , 'ses_parent' , 'Duplicate Task' )
)
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_dup' , 'ses_parent' , 'Duplicate Task' )
)
// then
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_dup' ] )
} )
test ( 'auto-attaches deferred sessions in FIFO order' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = >
createWindowState ( {
windowWidth : 160 ,
windowHeight : 11 ,
agentPanes : [
{
paneId : '%1' ,
width : 80 ,
height : 11 ,
left : 80 ,
top : 0 ,
title : 'old' ,
isActive : false ,
} ,
] ,
} )
)
const attachOrder : string [ ] = [ ]
mockExecuteActions . mockImplementation ( async ( actions ) = > {
for ( const action of actions ) {
if ( action . type === 'spawn' ) {
attachOrder . push ( action . sessionId )
trackedSessions . add ( action . sessionId )
return {
success : true ,
spawnedPaneId : ` % ${ action . sessionId } ` ,
results : [ { action , result : { success : true , paneId : ` % ${ action . sessionId } ` } } ] ,
}
}
}
return { success : true , results : [ ] }
} )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 120 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
await manager . onSessionCreated ( createSessionCreatedEvent ( 'ses_1' , 'ses_parent' , 'Task 1' ) )
await manager . onSessionCreated ( createSessionCreatedEvent ( 'ses_2' , 'ses_parent' , 'Task 2' ) )
await manager . onSessionCreated ( createSessionCreatedEvent ( 'ses_3' , 'ses_parent' , 'Task 3' ) )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_1' , 'ses_2' , 'ses_3' ] )
// when
mockQueryWindowState . mockImplementation ( async ( ) = > createWindowState ( ) )
await ( manager as any ) . tryAttachDeferredSession ( )
await ( manager as any ) . tryAttachDeferredSession ( )
await ( manager as any ) . tryAttachDeferredSession ( )
// then
expect ( attachOrder ) . toEqual ( [ 'ses_1' , 'ses_2' , 'ses_3' ] )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ ] )
} )
test ( 'does not attach deferred session more than once across repeated retries' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = >
createWindowState ( {
windowWidth : 160 ,
windowHeight : 11 ,
agentPanes : [
{
paneId : '%1' ,
width : 80 ,
height : 11 ,
left : 80 ,
top : 0 ,
title : 'old' ,
isActive : false ,
} ,
] ,
} )
)
let attachCount = 0
mockExecuteActions . mockImplementation ( async ( actions ) = > {
for ( const action of actions ) {
if ( action . type === 'spawn' ) {
attachCount += 1
trackedSessions . add ( action . sessionId )
return {
success : true ,
spawnedPaneId : ` % ${ action . sessionId } ` ,
results : [ { action , result : { success : true , paneId : ` % ${ action . sessionId } ` } } ] ,
}
}
}
return { success : true , results : [ ] }
} )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 120 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_once' , 'ses_parent' , 'Task Once' )
)
// when
mockQueryWindowState . mockImplementation ( async ( ) = > createWindowState ( ) )
await ( manager as any ) . tryAttachDeferredSession ( )
await ( manager as any ) . tryAttachDeferredSession ( )
// then
expect ( attachCount ) . toBe ( 1 )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ ] )
} )
test ( 'removes deferred session when session is deleted before attach' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = >
createWindowState ( {
windowWidth : 160 ,
windowHeight : 11 ,
agentPanes : [
{
paneId : '%1' ,
width : 80 ,
height : 11 ,
left : 80 ,
top : 0 ,
title : 'old' ,
isActive : false ,
} ,
] ,
} )
)
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 120 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_pending' , 'ses_parent' , 'Pending Task' )
)
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_pending' ] )
// when
await manager . onSessionDeleted ( { sessionID : 'ses_pending' } )
// then
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ ] )
expect ( mockExecuteAction ) . toHaveBeenCalledTimes ( 0 )
2026-01-25 15:34:10 +09:00
} )
2026-02-21 05:59:30 +09:00
describe ( 'spawn failure recovery' , ( ) = > {
test ( '#given queryWindowState returns null #when onSessionCreated fires #then session is enqueued in deferred queue' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = > null )
const logSpy = spyOn ( sharedModule , 'log' ) . mockImplementation ( ( ) = > { } )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
// when
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_null_state' , 'ses_parent' , 'Null State Task' )
)
// then
expect (
logSpy . mock . calls . some ( ( [ message ] ) = >
String ( message ) . includes ( 'failed to query window state, deferring session' )
)
) . toBe ( true )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_null_state' ] )
logSpy . mockRestore ( )
} )
test ( '#given spawn fails without close action #when onSessionCreated fires #then session is enqueued in deferred queue' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = > createWindowState ( ) )
mockExecuteActions . mockImplementation ( async ( actions ) = > ( {
success : false ,
spawnedPaneId : undefined ,
results : actions.map ( ( action ) = > ( {
action ,
result : { success : false , error : 'spawn failed' } ,
} ) ) ,
} ) )
const logSpy = spyOn ( sharedModule , 'log' ) . mockImplementation ( ( ) = > { } )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
// when
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_fail_no_close' , 'ses_parent' , 'Spawn Fail No Close' )
)
// then
expect (
logSpy . mock . calls . some ( ( [ message ] ) = >
String ( message ) . includes ( 're-queueing deferred session after spawn failure' )
)
) . toBe ( true )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_fail_no_close' ] )
logSpy . mockRestore ( )
} )
test ( '#given spawn fails with close action that succeeded #when onSessionCreated fires #then session is still enqueued in deferred queue' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
mockQueryWindowState . mockImplementation ( async ( ) = > createWindowState ( ) )
mockExecuteActions . mockImplementation ( async ( ) = > ( {
success : false ,
spawnedPaneId : undefined ,
results : [
{
action : { type : 'close' , paneId : '%1' , sessionId : 'ses_old' } ,
result : { success : true } ,
} ,
{
action : {
type : 'spawn' ,
sessionId : 'ses_fail_with_close' ,
description : 'Spawn Fail With Close' ,
targetPaneId : '%0' ,
splitDirection : '-h' ,
} ,
result : { success : false , error : 'spawn failed after close' } ,
} ,
] ,
} ) )
const logSpy = spyOn ( sharedModule , 'log' ) . mockImplementation ( ( ) = > { } )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
// when
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_fail_with_close' , 'ses_parent' , 'Spawn Fail With Close' )
)
// then
expect (
logSpy . mock . calls . some ( ( [ message ] ) = >
String ( message ) . includes ( 're-queueing deferred session after spawn failure' )
)
) . toBe ( true )
expect ( ( manager as any ) . deferredQueue ) . toEqual ( [ 'ses_fail_with_close' ] )
logSpy . mockRestore ( )
} )
} )
2026-01-25 15:34:10 +09:00
} )
2026-01-26 12:02:37 +09:00
describe ( 'onSessionDeleted' , ( ) = > {
2026-02-17 03:40:55 +09:00
test ( 'does not track session when readiness timed out' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
let stateCallCount = 0
mockQueryWindowState . mockImplementation ( async ( ) = > {
stateCallCount ++
if ( stateCallCount === 1 ) {
return createWindowState ( )
}
return createWindowState ( {
agentPanes : [
{
paneId : '%mock' ,
width : 40 ,
height : 44 ,
left : 100 ,
top : 0 ,
title : 'omo-subagent-Timeout Task' ,
isActive : false ,
} ,
] ,
} )
} )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( { sessionStatusResult : { data : { } } } )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_timeout' , 'ses_parent' , 'Timeout Task' )
)
mockExecuteAction . mockClear ( )
// when
await manager . onSessionDeleted ( { sessionID : 'ses_timeout' } )
// then
2026-02-20 07:13:33 +08:00
expect ( mockExecuteAction ) . toHaveBeenCalledTimes ( 1 )
2026-02-17 03:40:55 +09:00
} )
2026-01-26 12:02:37 +09:00
test ( 'closes pane when tracked session is deleted' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
2026-01-26 12:02:37 +09:00
let stateCallCount = 0
mockQueryWindowState . mockImplementation ( async ( ) = > {
stateCallCount ++
if ( stateCallCount === 1 ) {
return createWindowState ( )
}
return createWindowState ( {
agentPanes : [
{
paneId : '%mock' ,
width : 40 ,
2026-01-26 15:11:16 +09:00
height : 44 ,
left : 100 ,
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'omo-subagent-Task' ,
isActive : false ,
} ,
] ,
} )
2026-01-25 15:34:10 +09:00
} )
2026-01-26 12:02:37 +09:00
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
2026-01-25 15:34:10 +09:00
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated (
createSessionCreatedEvent (
'ses_child' ,
'ses_parent' ,
'Background: Test Task'
)
)
mockExecuteAction . mockClear ( )
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
await manager . onSessionDeleted ( { sessionID : 'ses_child' } )
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteAction ) . toHaveBeenCalledTimes ( 1 )
const call = mockExecuteAction . mock . calls [ 0 ]
expect ( call ) . toBeDefined ( )
expect ( call ! [ 0 ] ) . toEqual ( {
type : 'close' ,
paneId : '%mock' ,
sessionId : 'ses_child' ,
2026-01-25 15:34:10 +09:00
} )
2026-01-26 12:02:37 +09:00
} )
2026-01-25 15:34:10 +09:00
2026-04-01 18:18:32 -07:00
test ( '#given session isolation with a spawned container #when the first isolated subagent is deleted #then it cleans up the isolated container and clears the anchor pane id' , async ( ) = > {
// given
mockIsInsideTmux . mockReturnValue ( true )
let stateCallCount = 0
mockQueryWindowState . mockImplementation ( async ( paneId ) = > {
stateCallCount ++
if ( paneId === '%isolated-session-ses_first' ) {
return createWindowState ( {
mainPane : {
paneId ,
width : 110 ,
height : 44 ,
left : 0 ,
top : 0 ,
title : 'isolated' ,
isActive : true ,
} ,
} )
}
if ( stateCallCount === 1 ) {
return createWindowState ( )
}
return createWindowState ( {
mainPane : {
paneId : '%isolated-session-ses_first' ,
width : 110 ,
height : 44 ,
left : 0 ,
top : 0 ,
title : 'isolated' ,
isActive : true ,
} ,
} )
} )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
isolation : 'session' ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_first' , 'ses_parent' , 'First Task' )
)
mockExecuteAction . mockClear ( )
// when
await manager . onSessionDeleted ( { sessionID : 'ses_first' } )
// then
expect ( mockExecuteAction ) . toHaveBeenCalledTimes ( 1 )
expect ( mockExecuteAction . mock . calls [ 0 ] ? . [ 0 ] ) . toEqual ( {
type : 'close' ,
paneId : '%isolated-session-ses_first' ,
sessionId : 'ses_first' ,
} )
expect ( Reflect . get ( manager , 'isolatedWindowPaneId' ) ) . toBeUndefined ( )
} )
2026-01-26 12:02:37 +09:00
test ( 'does nothing when untracked session is deleted' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-26 12:02:37 +09:00
mockIsInsideTmux . mockReturnValue ( true )
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
await manager . onSessionDeleted ( { sessionID : 'ses_unknown' } )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteAction ) . toHaveBeenCalledTimes ( 0 )
2026-01-25 15:34:10 +09:00
} )
} )
describe ( 'cleanup' , ( ) = > {
test ( 'closes all tracked panes' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-25 15:34:10 +09:00
mockIsInsideTmux . mockReturnValue ( true )
2026-01-26 12:02:37 +09:00
let callCount = 0
2026-02-17 03:40:55 +09:00
mockExecuteActions . mockImplementation ( async ( actions ) = > {
2026-01-26 12:02:37 +09:00
callCount ++
2026-02-17 03:40:55 +09:00
for ( const action of actions ) {
if ( action . type === 'spawn' ) {
trackedSessions . add ( action . sessionId )
}
}
2026-01-26 12:02:37 +09:00
return {
success : true ,
spawnedPaneId : ` % ${ callCount } ` ,
results : [ ] ,
}
} )
2026-01-25 15:34:10 +09:00
const { TmuxSessionManager } = await import ( './manager' )
const ctx = createMockContext ( )
const config : TmuxConfig = {
enabled : true ,
layout : 'main-vertical' ,
main_pane_size : 60 ,
2026-01-26 12:02:37 +09:00
main_pane_min_width : 80 ,
agent_pane_min_width : 40 ,
2026-01-25 15:34:10 +09:00
}
2026-01-30 10:59:54 +09:00
const manager = new TmuxSessionManager ( ctx , config , mockTmuxDeps )
2026-01-25 15:34:10 +09:00
2026-01-26 12:02:37 +09:00
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_1' , 'ses_parent' , 'Task 1' )
)
await manager . onSessionCreated (
createSessionCreatedEvent ( 'ses_2' , 'ses_parent' , 'Task 2' )
)
mockExecuteAction . mockClear ( )
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
await manager . cleanup ( )
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( mockExecuteAction ) . toHaveBeenCalledTimes ( 2 )
} )
} )
2026-02-01 11:11:35 +01:00
2026-01-26 12:02:37 +09:00
} )
describe ( 'DecisionEngine' , ( ) = > {
describe ( 'calculateCapacity' , ( ) = > {
2026-01-26 15:11:16 +09:00
test ( 'calculates correct 2D grid capacity' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-26 12:02:37 +09:00
const { calculateCapacity } = await import ( './decision-engine' )
2026-02-01 16:47:50 +09:00
// when
2026-01-26 15:11:16 +09:00
const result = calculateCapacity ( 212 , 44 )
2026-01-26 12:02:37 +09:00
2026-02-01 16:47:50 +09:00
// then - availableWidth=106, cols=(106+1)/(52+1)=2, rows=(44+1)/(11+1)=3 (accounting for dividers)
2026-01-26 15:11:16 +09:00
expect ( result . cols ) . toBe ( 2 )
expect ( result . rows ) . toBe ( 3 )
expect ( result . total ) . toBe ( 6 )
2026-01-26 12:02:37 +09:00
} )
2026-01-26 15:11:16 +09:00
test ( 'returns 0 cols when agent area too narrow' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-26 12:02:37 +09:00
const { calculateCapacity } = await import ( './decision-engine' )
2026-02-01 16:47:50 +09:00
// when
2026-01-26 15:11:16 +09:00
const result = calculateCapacity ( 100 , 44 )
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then - availableWidth=50, cols=50/53=0
2026-01-26 15:11:16 +09:00
expect ( result . cols ) . toBe ( 0 )
expect ( result . total ) . toBe ( 0 )
2026-01-26 12:02:37 +09:00
} )
} )
2026-01-25 15:34:10 +09:00
2026-01-26 12:02:37 +09:00
describe ( 'decideSpawnActions' , ( ) = > {
2026-01-26 15:11:16 +09:00
test ( 'returns spawn action with splitDirection when under capacity' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-26 12:02:37 +09:00
const { decideSpawnActions } = await import ( './decision-engine' )
const state : WindowState = {
2026-01-26 15:11:16 +09:00
windowWidth : 212 ,
windowHeight : 44 ,
2026-01-26 12:02:37 +09:00
mainPane : {
paneId : '%0' ,
2026-01-26 15:11:16 +09:00
width : 106 ,
height : 44 ,
2026-01-26 12:02:37 +09:00
left : 0 ,
2026-01-26 15:11:16 +09:00
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'main' ,
isActive : true ,
} ,
agentPanes : [ ] ,
}
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
const decision = decideSpawnActions (
state ,
'ses_1' ,
'Test Task' ,
{ mainPaneMinWidth : 120 , agentPaneWidth : 40 } ,
[ ]
)
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( decision . canSpawn ) . toBe ( true )
expect ( decision . actions ) . toHaveLength ( 1 )
2026-01-26 15:11:16 +09:00
expect ( decision . actions [ 0 ] . type ) . toBe ( 'spawn' )
if ( decision . actions [ 0 ] . type === 'spawn' ) {
expect ( decision . actions [ 0 ] . sessionId ) . toBe ( 'ses_1' )
expect ( decision . actions [ 0 ] . description ) . toBe ( 'Test Task' )
expect ( decision . actions [ 0 ] . targetPaneId ) . toBe ( '%0' )
expect ( decision . actions [ 0 ] . splitDirection ) . toBe ( '-h' )
}
2026-01-26 12:02:37 +09:00
} )
2026-02-15 03:26:39 +08:00
test ( 'returns canSpawn=false when split not possible' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given - small window where split is never possible
2026-01-26 12:02:37 +09:00
const { decideSpawnActions } = await import ( './decision-engine' )
const state : WindowState = {
windowWidth : 160 ,
2026-01-26 15:11:16 +09:00
windowHeight : 11 ,
2026-01-26 12:02:37 +09:00
mainPane : {
paneId : '%0' ,
2026-01-26 15:11:16 +09:00
width : 80 ,
height : 11 ,
2026-01-26 12:02:37 +09:00
left : 0 ,
2026-01-26 15:11:16 +09:00
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'main' ,
isActive : true ,
} ,
agentPanes : [
{
paneId : '%1' ,
2026-01-26 15:11:16 +09:00
width : 80 ,
height : 11 ,
left : 80 ,
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'omo-subagent-Old' ,
isActive : false ,
} ,
] ,
}
const sessionMappings = [
{ sessionId : 'ses_old' , paneId : '%1' , createdAt : new Date ( '2024-01-01' ) } ,
]
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
const decision = decideSpawnActions (
state ,
'ses_new' ,
'New Task' ,
{ mainPaneMinWidth : 120 , agentPaneWidth : 40 } ,
sessionMappings
)
2026-02-15 03:26:39 +08:00
// then - agent area (80) < MIN_SPLIT_WIDTH (105), so attach is deferred
expect ( decision . canSpawn ) . toBe ( false )
expect ( decision . actions ) . toHaveLength ( 0 )
expect ( decision . reason ) . toContain ( 'defer' )
2026-01-26 12:02:37 +09:00
} )
2026-01-26 15:11:16 +09:00
test ( 'returns canSpawn=false when window too small' , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-26 12:02:37 +09:00
const { decideSpawnActions } = await import ( './decision-engine' )
const state : WindowState = {
2026-01-26 15:11:16 +09:00
windowWidth : 60 ,
windowHeight : 5 ,
2026-01-26 12:02:37 +09:00
mainPane : {
paneId : '%0' ,
2026-01-26 15:11:16 +09:00
width : 30 ,
height : 5 ,
2026-01-26 12:02:37 +09:00
left : 0 ,
2026-01-26 15:11:16 +09:00
top : 0 ,
2026-01-26 12:02:37 +09:00
title : 'main' ,
isActive : true ,
} ,
agentPanes : [ ] ,
}
2026-02-01 16:47:50 +09:00
// when
2026-01-26 12:02:37 +09:00
const decision = decideSpawnActions (
state ,
'ses_1' ,
'Test Task' ,
{ mainPaneMinWidth : 120 , agentPaneWidth : 40 } ,
[ ]
)
2026-01-25 15:34:10 +09:00
2026-02-01 16:47:50 +09:00
// then
2026-01-26 12:02:37 +09:00
expect ( decision . canSpawn ) . toBe ( false )
2026-01-26 15:11:16 +09:00
expect ( decision . reason ) . toContain ( 'too small' )
2026-01-25 15:34:10 +09:00
} )
} )
} )