test(tmux): add isolation regression coverage

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-04 01:21:04 +09:00
parent 53eeac3f31
commit b5f15f0371
2 changed files with 600 additions and 321 deletions
+16 -8
View File
@@ -1,14 +1,19 @@
declare module "bun:test" { declare module "bun:test" {
type AnyFunction = (...args: any[]) => any
interface MockMetadata<TArgs extends unknown[]> { interface MockMetadata<TArgs extends unknown[]> {
calls: TArgs[] calls: TArgs[]
} }
interface MockFunction<TArgs extends unknown[] = unknown[], TReturn = unknown> { interface MockFunction<TFunction extends AnyFunction = AnyFunction> {
(...args: TArgs): TReturn (...args: Parameters<TFunction>): ReturnType<TFunction>
mock: MockMetadata<TArgs> mock: MockMetadata<Parameters<TFunction>>
mockClear(): void
mockReset(): void mockReset(): void
mockReturnValue(value: TReturn): void mockRestore(): void
mockResolvedValue(value: Awaited<TReturn>): void mockReturnValue(value: ReturnType<TFunction>): void
mockResolvedValue(value: Awaited<ReturnType<TFunction>>): void
mockImplementation(fn: TFunction): MockFunction<TFunction>
} }
export function describe(name: string, fn: () => void): void export function describe(name: string, fn: () => void): void
@@ -18,9 +23,12 @@ declare module "bun:test" {
export function afterEach(fn: () => void | Promise<void>): void export function afterEach(fn: () => void | Promise<void>): void
export function beforeAll(fn: () => void | Promise<void>): void export function beforeAll(fn: () => void | Promise<void>): void
export function afterAll(fn: () => void | Promise<void>): void export function afterAll(fn: () => void | Promise<void>): void
export function mock<TArgs extends unknown[], TReturn>( export function mock<TFunction extends AnyFunction>(fn: TFunction): MockFunction<TFunction>
fn: (...args: TArgs) => TReturn,
): MockFunction<TArgs, TReturn> export function spyOn<TObject extends object>(
object: TObject,
key: keyof TObject,
): MockFunction<AnyFunction>
export namespace mock { export namespace mock {
function module(modulePath: string, factory: () => Record<string, unknown>): void function module(modulePath: string, factory: () => Record<string, unknown>): void
+399 -128
View File
@@ -1,3 +1,4 @@
/// <reference path="../../../bun-test.d.ts" />
import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:test' import { describe, test, expect, mock, beforeEach, spyOn, afterAll } from 'bun:test'
import type { TmuxConfig } from '../../config/schema' import type { TmuxConfig } from '../../config/schema'
import type { WindowState, PaneAction } from './types' import type { WindowState, PaneAction } from './types'
@@ -155,6 +156,18 @@ function createWindowState(overrides?: Partial<WindowState>): WindowState {
} }
} }
function createTmuxConfig(overrides?: Partial<TmuxConfig>): TmuxConfig {
return {
enabled: true,
isolation: 'inline',
layout: 'main-vertical',
main_pane_size: 60,
main_pane_min_width: 80,
agent_pane_min_width: 40,
...overrides,
}
}
describe('TmuxSessionManager', () => { describe('TmuxSessionManager', () => {
beforeEach(() => { beforeEach(() => {
mockQueryWindowState.mockClear() mockQueryWindowState.mockClear()
@@ -168,8 +181,7 @@ describe('TmuxSessionManager', () => {
trackedSessions.clear() trackedSessions.clear()
mockQueryWindowState.mockImplementation(async () => createWindowState()) mockQueryWindowState.mockImplementation(async () => createWindowState())
mockExecuteActions.mockImplementation(async (actions) => { mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => { for (const action of actions) {
for (const action of actions) {
if (action.type === 'spawn') { if (action.type === 'spawn') {
trackedSessions.add(action.sessionId) trackedSessions.add(action.sessionId)
} }
@@ -178,16 +190,15 @@ describe('TmuxSessionManager', () => {
success: true, success: true,
spawnedPaneId: '%mock', spawnedPaneId: '%mock',
results: [], results: [],
} } })
}) mockSpawnTmuxWindow.mockImplementation(async (sessionId: string) => {
mockSpawnTmuxWindow.mockImplementation(async (sessionId) => {
trackedSessions.add(sessionId) trackedSessions.add(sessionId)
return { return {
success: true, success: true,
paneId: `%isolated-window-${sessionId}`, paneId: `%isolated-window-${sessionId}`,
} }
}) })
mockSpawnTmuxSession.mockImplementation(async (sessionId) => { mockSpawnTmuxSession.mockImplementation(async (sessionId: string) => {
trackedSessions.add(sessionId) trackedSessions.add(sessionId)
return { return {
success: true, success: true,
@@ -210,13 +221,11 @@ describe('TmuxSessionManager', () => {
}, },
}, },
}) })
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
// when // when
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
@@ -236,13 +245,11 @@ describe('TmuxSessionManager', () => {
}, },
}, },
}) })
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
// when // when
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
@@ -256,13 +263,11 @@ describe('TmuxSessionManager', () => {
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: false,
enabled: false,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
// when // when
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
@@ -279,13 +284,11 @@ describe('TmuxSessionManager', () => {
...createMockContext(), ...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'), serverUrl: new URL('http://127.0.0.1:0/'),
} }
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
// when // when
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
@@ -303,13 +306,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
const event = createSessionCreatedEvent( const event = createSessionCreatedEvent(
'ses_child', 'ses_child',
@@ -364,13 +365,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when - first agent // when - first agent
@@ -396,8 +395,7 @@ describe('TmuxSessionManager', () => {
test('#given session isolation with healthy existing container #when second subagent is created #then it spawns inline from isolated pane', async () => { test('#given session isolation with healthy existing container #when second subagent is created #then it spawns inline from isolated pane', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
mockQueryWindowState.mockImplementation(async (paneId) => { mockQueryWindowState.mockImplementation(async (paneId: string) => { if (paneId === '%isolated-session-ses_first') {
if (paneId === '%isolated-session-ses_first') {
return createWindowState({ return createWindowState({
mainPane: { mainPane: {
paneId, paneId,
@@ -411,19 +409,16 @@ describe('TmuxSessionManager', () => {
}) })
} }
return createWindowState() return createWindowState() })
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
isolation: 'session', isolation: 'session',
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -458,18 +453,77 @@ describe('TmuxSessionManager', () => {
expect(context?.sourcePaneId).toBe('%isolated-session-ses_first') expect(context?.sourcePaneId).toBe('%isolated-session-ses_first')
}) })
test('#given window 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: string) => { if (paneId === '%isolated-window-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 = createTmuxConfig({ enabled: true,
isolation: 'window',
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(mockSpawnTmuxWindow).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-window-ses_first')
}
expect(context?.sourcePaneId).toBe('%isolated-window-ses_first')
})
test('does NOT spawn pane when session has no parentID', async () => { test('does NOT spawn pane when session has no parentID', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
const event = createSessionCreatedEvent('ses_root', undefined, 'Root Session') const event = createSessionCreatedEvent('ses_root', undefined, 'Root Session')
@@ -485,13 +539,11 @@ describe('TmuxSessionManager', () => {
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: false,
enabled: false,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
const event = createSessionCreatedEvent( const event = createSessionCreatedEvent(
'ses_child', 'ses_child',
@@ -511,13 +563,11 @@ describe('TmuxSessionManager', () => {
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
const event = { const event = {
type: 'session.deleted', type: 'session.deleted',
@@ -556,13 +606,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 120, main_pane_min_width: 120,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when // when
@@ -598,13 +646,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 120, main_pane_min_width: 120,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when // when
@@ -641,8 +687,7 @@ describe('TmuxSessionManager', () => {
) )
const attachOrder: string[] = [] const attachOrder: string[] = []
mockExecuteActions.mockImplementation(async (actions) => { mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => { for (const action of actions) {
for (const action of actions) {
if (action.type === 'spawn') { if (action.type === 'spawn') {
attachOrder.push(action.sessionId) attachOrder.push(action.sessionId)
trackedSessions.add(action.sessionId) trackedSessions.add(action.sessionId)
@@ -653,18 +698,15 @@ describe('TmuxSessionManager', () => {
} }
} }
} }
return { success: true, results: [] } return { success: true, results: [] } })
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 120, main_pane_min_width: 120,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated(createSessionCreatedEvent('ses_1', 'ses_parent', 'Task 1')) await manager.onSessionCreated(createSessionCreatedEvent('ses_1', 'ses_parent', 'Task 1'))
@@ -705,8 +747,7 @@ describe('TmuxSessionManager', () => {
) )
let attachCount = 0 let attachCount = 0
mockExecuteActions.mockImplementation(async (actions) => { mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => { for (const action of actions) {
for (const action of actions) {
if (action.type === 'spawn') { if (action.type === 'spawn') {
attachCount += 1 attachCount += 1
trackedSessions.add(action.sessionId) trackedSessions.add(action.sessionId)
@@ -717,18 +758,15 @@ describe('TmuxSessionManager', () => {
} }
} }
} }
return { success: true, results: [] } return { success: true, results: [] } })
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 120, main_pane_min_width: 120,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -768,13 +806,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 120, main_pane_min_width: 120,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -791,6 +827,42 @@ describe('TmuxSessionManager', () => {
}) })
describe('spawn failure recovery', () => { describe('spawn failure recovery', () => {
test('#given the first isolated container spawn fails #when onSessionCreated fires #then the session is deferred for retry', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
mockSpawnTmuxSession.mockImplementation(async () => ({
success: false,
}))
const logSpy = spyOn(sharedModule, 'log').mockImplementation(() => {})
const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext()
const config = createTmuxConfig({ 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)
// when
await manager.onSessionCreated(
createSessionCreatedEvent('ses_isolated_fail', 'ses_parent', 'Isolated Failure Task')
)
// then
expect(mockSpawnTmuxSession).toHaveBeenCalledTimes(1)
expect(mockExecuteActions).toHaveBeenCalledTimes(0)
expect(
logSpy.mock.calls.some(([message]) =>
String(message).includes('isolated container failed, deferring session for retry')
)
).toBe(true)
expect(Reflect.get(manager, 'deferredQueue')).toEqual(['ses_isolated_fail'])
logSpy.mockRestore()
})
test('#given queryWindowState returns null #when onSessionCreated fires #then session is enqueued in deferred queue', async () => { test('#given queryWindowState returns null #when onSessionCreated fires #then session is enqueued in deferred queue', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
@@ -799,13 +871,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when // when
@@ -824,14 +894,70 @@ describe('TmuxSessionManager', () => {
logSpy.mockRestore() logSpy.mockRestore()
}) })
test('#given isolated window state returns one transient null #when another subagent is created #then the existing container is reused', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const isolatedPaneId = '%isolated-session-ses_first'
let isolatedPaneQueryCount = 0
mockQueryWindowState.mockImplementation(async (paneId: string) => { if (paneId === isolatedPaneId) {
isolatedPaneQueryCount += 1
if (isolatedPaneQueryCount === 1) {
return null
}
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 = createTmuxConfig({ 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')
)
mockSpawnTmuxSession.mockClear()
mockExecuteActions.mockClear()
// when
await manager.onSessionCreated(
createSessionCreatedEvent('ses_second', 'ses_parent', 'Second Task')
)
// then
expect(mockSpawnTmuxSession).toHaveBeenCalledTimes(0)
expect(mockExecuteActions).toHaveBeenCalledTimes(1)
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBe(isolatedPaneId)
expect(mockExecuteActions.mock.calls[0]?.[1]?.sourcePaneId).toBe(isolatedPaneId)
})
test('#given spawn fails without close action #when onSessionCreated fires #then session is enqueued in deferred queue', async () => { test('#given spawn fails without close action #when onSessionCreated fires #then session is enqueued in deferred queue', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
mockQueryWindowState.mockImplementation(async () => createWindowState()) mockQueryWindowState.mockImplementation(async () => createWindowState())
mockExecuteActions.mockImplementation(async (actions) => ({ mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => ({
success: false, success: false,
spawnedPaneId: undefined, spawnedPaneId: undefined,
results: actions.map((action) => ({ results: actions.map((action: PaneAction) => ({
action, action,
result: { success: false, error: 'spawn failed' }, result: { success: false, error: 'spawn failed' },
})), })),
@@ -840,13 +966,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when // when
@@ -893,13 +1017,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when // when
@@ -947,13 +1069,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext({ sessionStatusResult: { data: {} } }) const ctx = createMockContext({ sessionStatusResult: { data: {} } })
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -995,13 +1115,11 @@ describe('TmuxSessionManager', () => {
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -1032,8 +1150,7 @@ describe('TmuxSessionManager', () => {
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
let stateCallCount = 0 let stateCallCount = 0
mockQueryWindowState.mockImplementation(async (paneId) => { mockQueryWindowState.mockImplementation(async (paneId: string) => { stateCallCount++
stateCallCount++
if (paneId === '%isolated-session-ses_first') { if (paneId === '%isolated-session-ses_first') {
return createWindowState({ return createWindowState({
@@ -1063,19 +1180,16 @@ describe('TmuxSessionManager', () => {
title: 'isolated', title: 'isolated',
isActive: true, isActive: true,
}, },
}) }) })
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
isolation: 'session', isolation: 'session',
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -1097,11 +1211,76 @@ describe('TmuxSessionManager', () => {
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined() expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined()
}) })
test('#given window 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: string) => { stateCallCount += 1
if (paneId === '%isolated-window-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-window-ses_first',
width: 110,
height: 44,
left: 0,
top: 0,
title: 'isolated',
isActive: true,
},
}) })
const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext()
const config = createTmuxConfig({ enabled: true,
isolation: 'window',
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-window-ses_first',
sessionId: 'ses_first',
})
expect(Reflect.get(manager, 'isolatedContainerPaneId')).toBeUndefined()
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined()
})
test('#given session isolation with another subagent still tracked #when the anchor subagent is deleted first #then it reassigns the anchor and cleans up when the last subagent exits', async () => { test('#given session isolation with another subagent still tracked #when the anchor subagent is deleted first #then it reassigns the anchor and cleans up when the last subagent exits', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
mockQueryWindowState.mockImplementation(async (paneId) => { mockQueryWindowState.mockImplementation(async (paneId: string) => { if (paneId === '%isolated-session-ses_first') {
if (paneId === '%isolated-session-ses_first') {
return createWindowState({ return createWindowState({
mainPane: { mainPane: {
paneId, paneId,
@@ -1151,19 +1330,16 @@ describe('TmuxSessionManager', () => {
}) })
} }
return createWindowState() return createWindowState() })
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
isolation: 'session', isolation: 'session',
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(
@@ -1202,18 +1378,117 @@ describe('TmuxSessionManager', () => {
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined() expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined()
}) })
test('#given window isolation with another subagent still tracked #when the anchor subagent is deleted first #then it reassigns the anchor and cleans up when the last subagent exits', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
mockQueryWindowState.mockImplementation(async (paneId: string) => { if (paneId === '%isolated-window-ses_first') {
return createWindowState({
mainPane: {
paneId,
width: 110,
height: 44,
left: 0,
top: 0,
title: 'isolated',
isActive: true,
},
agentPanes: [
{
paneId: '%mock',
width: 40,
height: 44,
left: 110,
top: 0,
title: 'omo-subagent-Second Task',
isActive: false,
},
],
})
}
if (paneId === '%mock') {
return createWindowState({
mainPane: {
paneId: '%isolated-window-ses_first',
width: 110,
height: 44,
left: 0,
top: 0,
title: 'isolated',
isActive: true,
},
agentPanes: [
{
paneId,
width: 40,
height: 44,
left: 110,
top: 0,
title: 'omo-subagent-Second Task',
isActive: false,
},
],
})
}
return createWindowState() })
const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext()
const config = createTmuxConfig({ enabled: true,
isolation: 'window',
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')
)
await manager.onSessionCreated(
createSessionCreatedEvent('ses_second', 'ses_parent', 'Second Task')
)
mockExecuteAction.mockClear()
// when
await manager.onSessionDeleted({ sessionID: 'ses_first' })
// then
expect(mockExecuteAction).toHaveBeenCalledTimes(0)
expect(Reflect.get(manager, 'isolatedContainerPaneId')).toBe('%isolated-window-ses_first')
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBe('%mock')
// when
await manager.onSessionDeleted({ sessionID: 'ses_second' })
// then
expect(mockExecuteAction).toHaveBeenCalledTimes(2)
expect(mockExecuteAction.mock.calls[0]?.[0]).toEqual({
type: 'close',
paneId: '%mock',
sessionId: 'ses_second',
})
expect(mockExecuteAction.mock.calls[1]?.[0]).toEqual({
type: 'close',
paneId: '%isolated-window-ses_first',
sessionId: 'ses_second',
})
expect(Reflect.get(manager, 'isolatedContainerPaneId')).toBeUndefined()
expect(Reflect.get(manager, 'isolatedWindowPaneId')).toBeUndefined()
})
test('does nothing when untracked session is deleted', async () => { test('does nothing when untracked session is deleted', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when // when
@@ -1230,8 +1505,7 @@ describe('TmuxSessionManager', () => {
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
let callCount = 0 let callCount = 0
mockExecuteActions.mockImplementation(async (actions) => { mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => { callCount++
callCount++
for (const action of actions) { for (const action of actions) {
if (action.type === 'spawn') { if (action.type === 'spawn') {
trackedSessions.add(action.sessionId) trackedSessions.add(action.sessionId)
@@ -1241,18 +1515,15 @@ describe('TmuxSessionManager', () => {
success: true, success: true,
spawnedPaneId: `%${callCount}`, spawnedPaneId: `%${callCount}`,
results: [], results: [],
} } })
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext() const ctx = createMockContext()
const config: TmuxConfig = { const config = createTmuxConfig({ enabled: true,
enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
main_pane_min_width: 80, main_pane_min_width: 80,
agent_pane_min_width: 40, agent_pane_min_width: 40, })
}
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
await manager.onSessionCreated( await manager.onSessionCreated(