test(tmux-subagent): expand manager test coverage

This commit is contained in:
YeonGyu-Kim
2026-04-28 10:45:26 +09:00
parent 680927d0fd
commit 77655a7c30
+511 -77
View File
@@ -17,6 +17,11 @@ type SpawnTmuxContainerResult = {
paneId?: string paneId?: string
} }
type SessionReadyWaitParams = {
client: unknown
sessionId: string
}
const mockQueryWindowState = mock<(paneId: string) => Promise<WindowState | null>>( const mockQueryWindowState = mock<(paneId: string) => Promise<WindowState | null>>(
async () => ({ async () => ({
windowWidth: 212, windowWidth: 212,
@@ -38,6 +43,13 @@ const mockExecuteAction = mock<(
action: PaneAction, action: PaneAction,
ctx: ExecuteContext ctx: ExecuteContext
) => Promise<ActionResult>>(async () => ({ success: true })) ) => Promise<ActionResult>>(async () => ({ success: true }))
const mockSpawnTmuxPane = mock(async (_sessionId?: string) => ({
success: true,
paneId: '%mock',
}))
const mockWaitForSessionReady = mock<(
params: SessionReadyWaitParams,
) => Promise<boolean>>(async () => true)
const mockSpawnTmuxWindow = mock<( const mockSpawnTmuxWindow = mock<(
sessionId: string, sessionId: string,
description: string, description: string,
@@ -65,49 +77,44 @@ const mockGetCurrentPaneId = mock<() => string | undefined>(() => '%0')
const mockTmuxDeps: TmuxUtilDeps = { const mockTmuxDeps: TmuxUtilDeps = {
isInsideTmux: mockIsInsideTmux, isInsideTmux: mockIsInsideTmux,
getCurrentPaneId: mockGetCurrentPaneId, getCurrentPaneId: mockGetCurrentPaneId,
queryWindowState: mockQueryWindowState,
} }
mock.module('./pane-state-querier', () => ({ function registerModuleMocks(): void {
queryWindowState: mockQueryWindowState, mock.module('./action-executor', () => ({
paneExists: mockPaneExists, executeActions: mockExecuteActions,
getRightmostAgentPane: (state: WindowState) => executeAction: mockExecuteAction,
state.agentPanes.length > 0 executeActionWithDeps: mockExecuteAction,
? state.agentPanes.reduce((r, p) => (p.left > r.left ? p : r)) }))
: null,
getOldestAgentPane: (state: WindowState) => mock.module('./session-ready-waiter', () => ({
state.agentPanes.length > 0 waitForSessionReady: mockWaitForSessionReady,
? state.agentPanes.reduce((o, p) => (p.left < o.left ? p : o)) }))
: null,
})) 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,
spawnTmuxWindow: mockSpawnTmuxWindow,
spawnTmuxSession: mockSpawnTmuxSession,
killTmuxSessionIfExists: mockKillTmuxSessionIfExists,
getIsolatedSessionName: (pid: number = 12345) => `omo-agents-${pid}`,
sweepStaleOmoAgentSessions: mockSweepStaleOmoAgentSessions,
}
})
}
afterAll(() => { mock.restore() }) afterAll(() => { mock.restore() })
mock.module('./action-executor', () => ({
executeActions: mockExecuteActions,
executeAction: mockExecuteAction,
executeActionWithDeps: mockExecuteAction,
}))
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,
spawnTmuxWindow: mockSpawnTmuxWindow,
spawnTmuxSession: mockSpawnTmuxSession,
killTmuxSessionIfExists: mockKillTmuxSessionIfExists,
getIsolatedSessionName: (pid: number = 12345) => `omo-agents-${pid}`,
sweepStaleOmoAgentSessions: mockSweepStaleOmoAgentSessions,
}
})
const trackedSessions = new Set<string>() const trackedSessions = new Set<string>()
const readySessions = new Set<string>()
function createMockContext(overrides?: { function createMockContext(overrides?: {
sessionStatusResult?: { data?: Record<string, { type: string }> } sessionStatusResult?: { data?: Record<string, { type: string }> }
@@ -125,6 +132,9 @@ function createMockContext(overrides?: {
for (const sessionId of trackedSessions) { for (const sessionId of trackedSessions) {
data[sessionId] = { type: 'running' } data[sessionId] = { type: 'running' }
} }
for (const sessionId of readySessions) {
data[sessionId] = { type: 'running' }
}
return { data } return { data }
}), }),
messages: mock(async () => { messages: mock(async () => {
@@ -161,6 +171,28 @@ function createWindowState(overrides?: Partial<WindowState>): WindowState {
} }
} }
function createDeferred<TValue>() {
let resolvePromise!: (value: TValue | PromiseLike<TValue>) => void
let rejectPromise!: (reason?: unknown) => void
const promise = new Promise<TValue>((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
return {
promise,
resolve: resolvePromise,
reject: rejectPromise,
}
}
async function flushMicrotasks(turns: number = 5): Promise<void> {
for (let index = 0; index < turns; index += 1) {
await Promise.resolve()
}
}
function createTmuxConfig(overrides?: Partial<TmuxConfig>): TmuxConfig { function createTmuxConfig(overrides?: Partial<TmuxConfig>): TmuxConfig {
return { return {
enabled: true, enabled: true,
@@ -177,29 +209,57 @@ function getTrackedSessions(manager: object): Map<string, { paneId: string; clos
return Reflect.get(manager, 'sessions') as Map<string, { paneId: string; closePending: boolean; closeRetryCount: number }> return Reflect.get(manager, 'sessions') as Map<string, { paneId: string; closePending: boolean; closeRetryCount: number }>
} }
function getFailedReadinessSessions(manager: object): Map<string, { sessionId: string; title: string }> {
return Reflect.get(manager, 'failedReadinessSessions') as Map<string, { sessionId: string; title: string }>
}
describe('TmuxSessionManager', () => { describe('TmuxSessionManager', () => {
beforeEach(() => { beforeEach(() => {
mock.restore()
registerModuleMocks()
mockQueryWindowState.mockClear() mockQueryWindowState.mockClear()
mockPaneExists.mockClear() mockPaneExists.mockClear()
mockExecuteActions.mockClear() mockExecuteActions.mockClear()
mockExecuteAction.mockClear() mockExecuteAction.mockClear()
mockSpawnTmuxPane.mockClear()
mockWaitForSessionReady.mockClear()
mockSpawnTmuxWindow.mockClear() mockSpawnTmuxWindow.mockClear()
mockSpawnTmuxSession.mockClear() mockSpawnTmuxSession.mockClear()
mockIsInsideTmux.mockClear() mockIsInsideTmux.mockClear()
mockGetCurrentPaneId.mockClear() mockGetCurrentPaneId.mockClear()
trackedSessions.clear() trackedSessions.clear()
readySessions.clear()
mockQueryWindowState.mockImplementation(async () => createWindowState()) mockQueryWindowState.mockImplementation(async () => createWindowState())
mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => { for (const action of actions) { mockExecuteActions.mockImplementation(async (actions: PaneAction[]) => {
if (action.type === 'spawn') { const results: ExecuteActionsResult['results'] = []
trackedSessions.add(action.sessionId) let spawnedPaneId: string | undefined
for (const action of actions) {
if (action.type === 'spawn') {
const spawnResult = await mockSpawnTmuxPane(action.sessionId)
if (!spawnResult.success) {
return {
success: false,
results: [{ action, result: { success: false, error: 'spawn failed' } }],
}
}
trackedSessions.add(action.sessionId)
spawnedPaneId = spawnResult.paneId
results.push({ action, result: { success: true, paneId: spawnResult.paneId } })
}
} }
}
return { return {
success: true, success: true,
spawnedPaneId: '%mock', spawnedPaneId: spawnedPaneId ?? '%mock',
results: [], results,
} }) }
})
mockWaitForSessionReady.mockImplementation(async ({ sessionId }: SessionReadyWaitParams) => {
readySessions.add(sessionId)
return true
})
mockSpawnTmuxWindow.mockImplementation(async (sessionId: string) => { mockSpawnTmuxWindow.mockImplementation(async (sessionId: string) => {
trackedSessions.add(sessionId) trackedSessions.add(sessionId)
return { return {
@@ -382,6 +442,50 @@ describe('TmuxSessionManager', () => {
}) })
}) })
describe('getServerUrl', () => {
test('returns normalized serverUrl from ctx', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:12345/'),
}
const config = createTmuxConfig({ enabled: true })
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when
const serverUrl = manager.getServerUrl()
// then
expect(serverUrl).toBe('http://127.0.0.1:12345/')
})
test('returns fallback when port is 0', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const originalPort = process.env.OPENCODE_PORT
delete process.env.OPENCODE_PORT
const { TmuxSessionManager } = await import('./manager')
const ctx = {
...createMockContext(),
serverUrl: new URL('http://127.0.0.1:0/'),
}
const config = createTmuxConfig({ enabled: true })
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
// when
const serverUrl = manager.getServerUrl()
// then
try {
expect(serverUrl).toBe(`http://localhost:${process.env.OPENCODE_PORT ?? '4096'}`)
} finally {
if (originalPort !== undefined) process.env.OPENCODE_PORT = originalPort
}
})
})
describe('onSessionCreated', () => { describe('onSessionCreated', () => {
test('first agent spawns from source pane via decision engine', async () => { test('first agent spawns from source pane via decision engine', async () => {
// given // given
@@ -867,6 +971,86 @@ describe('TmuxSessionManager', () => {
expect((manager as any).deferredQueue).toEqual([]) expect((manager as any).deferredQueue).toEqual([])
}) })
test('skips deferred attach when the session is already pending through another spawn path', 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 manager = new TmuxSessionManager(createMockContext(), createTmuxConfig({ enabled: true }), mockTmuxDeps)
await manager.onSessionCreated(
createSessionCreatedEvent('ses_pending_race', 'ses_parent', 'Pending Race Task')
)
expect((manager as any).deferredQueue).toEqual(['ses_pending_race'])
mockQueryWindowState.mockImplementation(async () => createWindowState())
Reflect.get(manager, 'pendingSessions').add('ses_pending_race')
// when
await Reflect.get(manager, 'tryAttachDeferredSession').call(manager)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect((manager as any).deferredQueue).toEqual(['ses_pending_race'])
})
test('drops deferred sessions that were already closed by polling', 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 manager = new TmuxSessionManager(createMockContext(), createTmuxConfig({ enabled: true }), mockTmuxDeps)
await manager.onSessionCreated(
createSessionCreatedEvent('ses_bounce', 'ses_parent', 'Bounce Task')
)
expect((manager as any).deferredQueue).toEqual(['ses_bounce'])
mockQueryWindowState.mockImplementation(async () => createWindowState())
Reflect.set(manager, 'closedByPolling', new Set(['ses_bounce']))
// when
await Reflect.get(manager, 'tryAttachDeferredSession').call(manager)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect((manager as any).deferredQueue).toEqual([])
})
test('removes deferred session when session is deleted before attach', async () => { test('removes deferred session when session is deleted before attach', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
@@ -1137,55 +1321,304 @@ describe('TmuxSessionManager', () => {
}) })
}) })
test('#given session.status never reports session ready #when onSessionCreated runs #then pane is tracked immediately without blocking', async () => { test('#given session readiness is pending #when onSessionCreated runs #then pane spawn waits until readiness resolves', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
mockQueryWindowState.mockImplementation(async () => createWindowState()) mockQueryWindowState.mockImplementation(async () => createWindowState())
const readiness = createDeferred<boolean>()
mockWaitForSessionReady.mockImplementationOnce(async ({ sessionId }: SessionReadyWaitParams) => {
const ready = await readiness.promise
if (ready) {
readySessions.add(sessionId)
}
return ready
})
const { TmuxSessionManager } = await import('./manager') const { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext({ sessionStatusResult: { data: {} } }) const ctx = createMockContext()
const config = createTmuxConfig({ enabled: true }) const config = createTmuxConfig({ enabled: true })
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps) const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
const event = createSessionCreatedEvent('ses_fast_track', 'ses_parent', 'Fast Track') const event = createSessionCreatedEvent('ses_wait', 'ses_parent', 'Wait For Ready')
// when // when
const start = Date.now() const onSessionCreatedPromise = manager.onSessionCreated(event)
await manager.onSessionCreated(event) await flushMicrotasks()
const elapsed = Date.now() - start
// then // then
expect(elapsed < 500).toBe(true) expect(mockWaitForSessionReady).toHaveBeenCalledTimes(1)
expect(getTrackedSessions(manager).has('ses_fast_track')).toBe(true) expect(mockExecuteActions).toHaveBeenCalledTimes(0)
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
// when
readiness.resolve(true)
await onSessionCreatedPromise
// then
expect(mockExecuteActions).toHaveBeenCalledTimes(1)
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(1)
expect(getTrackedSessions(manager).has('ses_wait')).toBe(true)
})
test('#given readiness probe fails #when onSessionCreated runs #then it logs the structured error and does not spawn a pane', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const readinessError = new Error('session readiness timed out')
mockWaitForSessionReady.mockImplementationOnce(async () => {
throw readinessError
})
const logSpy = spyOn(sharedModule, 'log').mockImplementation(() => {})
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(createMockContext(), createTmuxConfig({ enabled: true }), mockTmuxDeps)
// when
await manager.onSessionCreated(
createSessionCreatedEvent('ses_timeout', 'ses_parent', 'Timeout Task')
)
// then
expect(mockExecuteActions).toHaveBeenCalledTimes(0)
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect(logSpy).toHaveBeenCalledWith(
'[tmux-session-manager] session readiness failed before spawn',
expect.objectContaining({
sessionId: 'ses_timeout',
stage: 'session.created',
error: String(readinessError),
}),
)
logSpy.mockRestore()
})
test("skips pane creation when session exists but status is 'error'", async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
mockWaitForSessionReady.mockImplementationOnce(async () => true)
const logSpy = spyOn(sharedModule, 'log').mockImplementation(() => {})
const sessionStatusResult = {
data: {
ses_error: { type: 'error' },
},
}
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(
createMockContext({ sessionStatusResult }),
createTmuxConfig({ enabled: true }),
mockTmuxDeps,
)
// when
await manager.onSessionCreated(
createSessionCreatedEvent('ses_error', 'ses_parent', 'Errored Session')
)
// then
expect(mockExecuteActions).toHaveBeenCalledTimes(0)
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect(getTrackedSessions(manager).has('ses_error')).toBe(false)
expect(getFailedReadinessSessions(manager).has('ses_error')).toBe(true)
expect(logSpy).toHaveBeenCalledWith(
'[tmux-session-manager] session not attachable for pane spawn',
expect.objectContaining({
sessionId: 'ses_error',
stage: 'session.created',
status: 'error',
}),
)
logSpy.mockRestore()
})
test('retries pane creation on session.idle after a readiness timeout when status becomes attachable', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const readinessError = new Error('session readiness timed out')
mockWaitForSessionReady
.mockImplementationOnce(async () => {
throw readinessError
})
.mockImplementationOnce(async () => true)
const sessionStatusResult = {
data: {} as Record<string, { type: string }>,
}
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(
createMockContext({ sessionStatusResult }),
createTmuxConfig({ enabled: true }),
mockTmuxDeps,
)
// when
await manager.onSessionCreated(
createSessionCreatedEvent('ses_retry', 'ses_parent', 'Retry Session')
)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect(getFailedReadinessSessions(manager).has('ses_retry')).toBe(true)
// when
sessionStatusResult.data.ses_retry = { type: 'idle' }
manager.onEvent({ type: 'session.idle', properties: { sessionID: 'ses_retry' } })
await flushMicrotasks(20)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(1)
expect(getTrackedSessions(manager).has('ses_retry')).toBe(true)
expect(getFailedReadinessSessions(manager).has('ses_retry')).toBe(false)
})
test('does not retry more than once per sessionID', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
mockWaitForSessionReady
.mockImplementationOnce(async () => {
throw new Error('session readiness timed out')
})
.mockImplementationOnce(async () => true)
const sessionStatusResult = {
data: {
ses_retry_once: { type: 'idle' },
},
}
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(
createMockContext({ sessionStatusResult }),
createTmuxConfig({ enabled: true }),
mockTmuxDeps,
)
// when
await manager.onSessionCreated(
createSessionCreatedEvent('ses_retry_once', 'ses_parent', 'Retry Once Session')
)
manager.onEvent({ type: 'session.idle', properties: { sessionID: 'ses_retry_once' } })
await flushMicrotasks(20)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(1)
expect(getFailedReadinessSessions(manager).has('ses_retry_once')).toBe(false)
// when
manager.onEvent({ type: 'session.idle', properties: { sessionID: 'ses_retry_once' } })
await flushMicrotasks(20)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(1)
})
test('expires failed readiness sessions after the TTL elapses', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const nowSpy = spyOn(Date, 'now')
nowSpy.mockReturnValue(0)
mockWaitForSessionReady.mockImplementationOnce(async () => {
throw new Error('session readiness timed out')
})
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(
createMockContext({ sessionStatusResult: { data: { ses_expired: { type: 'idle' } } } }),
createTmuxConfig({ enabled: true }),
mockTmuxDeps,
)
await manager.onSessionCreated(
createSessionCreatedEvent('ses_expired', 'ses_parent', 'Expired Retry Session')
)
expect(getFailedReadinessSessions(manager).has('ses_expired')).toBe(true)
// when
nowSpy.mockReturnValue(5 * 60 * 1000 + 1)
manager.onEvent({ type: 'session.idle', properties: { sessionID: 'ses_expired' } })
await flushMicrotasks(20)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect(getFailedReadinessSessions(manager).has('ses_expired')).toBe(false)
nowSpy.mockRestore()
})
test('does not retry failed readiness sessions after polling marked the session closed', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(
createMockContext({ sessionStatusResult: { data: { ses_bounce: { type: 'idle' } } } }),
createTmuxConfig({ enabled: true }),
mockTmuxDeps,
)
Reflect.get(manager, 'failedReadinessSessions').set('ses_bounce', {
sessionId: 'ses_bounce',
title: 'Bounce Session',
rememberedAt: Date.now(),
})
Reflect.set(manager, 'closedByPolling', new Set(['ses_bounce']))
// when
manager.onEvent({ type: 'session.idle', properties: { sessionID: 'ses_bounce' } })
await flushMicrotasks(20)
// then
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
expect(getFailedReadinessSessions(manager).has('ses_bounce')).toBe(true)
})
test('#given duplicate session.created triggers while readiness is pending #when readiness resolves #then only one pane spawn runs', async () => {
// given
mockIsInsideTmux.mockReturnValue(true)
const readiness = createDeferred<boolean>()
mockWaitForSessionReady.mockImplementationOnce(async ({ sessionId }: SessionReadyWaitParams) => {
const ready = await readiness.promise
if (ready) {
readySessions.add(sessionId)
}
return ready
})
const { TmuxSessionManager } = await import('./manager')
const manager = new TmuxSessionManager(createMockContext(), createTmuxConfig({ enabled: true }), mockTmuxDeps)
const event = createSessionCreatedEvent('ses_dup_pending', 'ses_parent', 'Duplicate Pending')
// when
const firstSpawnPromise = manager.onSessionCreated(event)
const secondSpawnPromise = manager.onSessionCreated(event)
await flushMicrotasks()
// then
expect(mockWaitForSessionReady).toHaveBeenCalledTimes(1)
expect(mockExecuteActions).toHaveBeenCalledTimes(0)
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(0)
// when
readiness.resolve(true)
await Promise.all([firstSpawnPromise, secondSpawnPromise])
// then
expect(mockWaitForSessionReady).toHaveBeenCalledTimes(1)
expect(mockExecuteActions).toHaveBeenCalledTimes(1)
expect(mockSpawnTmuxPane).toHaveBeenCalledTimes(1)
expect(getTrackedSessions(manager).has('ses_dup_pending')).toBe(true)
}) })
}) })
describe('onSessionDeleted', () => { describe('onSessionDeleted', () => {
test('does not track session when readiness timed out', async () => { test('does nothing when session creation stopped before tracking due to readiness failure', async () => {
// given // given
mockIsInsideTmux.mockReturnValue(true) mockIsInsideTmux.mockReturnValue(true)
let stateCallCount = 0 mockWaitForSessionReady.mockImplementationOnce(async () => {
mockQueryWindowState.mockImplementation(async () => { throw new Error('readiness failed')
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 { TmuxSessionManager } = await import('./manager')
const ctx = createMockContext({ sessionStatusResult: { data: {} } }) const ctx = createMockContext()
const config = createTmuxConfig({ enabled: true, const config = createTmuxConfig({ enabled: true,
layout: 'main-vertical', layout: 'main-vertical',
main_pane_size: 60, main_pane_size: 60,
@@ -1202,7 +1635,7 @@ describe('TmuxSessionManager', () => {
await manager.onSessionDeleted({ sessionID: 'ses_timeout' }) await manager.onSessionDeleted({ sessionID: 'ses_timeout' })
// then // then
expect(mockExecuteAction).toHaveBeenCalledTimes(1) expect(mockExecuteAction).toHaveBeenCalledTimes(0)
}) })
test('closes pane when tracked session is deleted', async () => { test('closes pane when tracked session is deleted', async () => {
@@ -2064,7 +2497,8 @@ describe('TmuxSessionManager', () => {
const cleanupPromise = manager.cleanup() const cleanupPromise = manager.cleanup()
// then // then
expect(await cleanupPromise).toBeUndefined() const cleanupResult = await cleanupPromise
expect(cleanupResult).toBeUndefined()
expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1) expect(mockKillTmuxSessionIfExists).toHaveBeenCalledTimes(1)
}) })
}) })