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:
Vendored
+16
-8
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user