Merge branch 'fix/ub3-regression' into dev
This commit is contained in:
@@ -11,6 +11,11 @@ type ExecuteActionsResult = {
|
|||||||
results: Array<{ action: PaneAction; result: ActionResult }>
|
results: Array<{ action: PaneAction; result: ActionResult }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SpawnTmuxContainerResult = {
|
||||||
|
success: boolean
|
||||||
|
paneId?: string
|
||||||
|
}
|
||||||
|
|
||||||
const mockQueryWindowState = mock<(paneId: string) => Promise<WindowState | null>>(
|
const mockQueryWindowState = mock<(paneId: string) => Promise<WindowState | null>>(
|
||||||
async () => ({
|
async () => ({
|
||||||
windowWidth: 212,
|
windowWidth: 212,
|
||||||
@@ -32,6 +37,25 @@ const mockExecuteAction = mock<(
|
|||||||
action: PaneAction,
|
action: PaneAction,
|
||||||
ctx: ExecuteContext
|
ctx: ExecuteContext
|
||||||
) => Promise<ActionResult>>(async () => ({ success: true }))
|
) => Promise<ActionResult>>(async () => ({ success: true }))
|
||||||
|
const mockSpawnTmuxWindow = mock<(
|
||||||
|
sessionId: string,
|
||||||
|
description: string,
|
||||||
|
config: TmuxConfig,
|
||||||
|
serverUrl: string
|
||||||
|
) => Promise<SpawnTmuxContainerResult>>(async () => ({
|
||||||
|
success: true,
|
||||||
|
paneId: '%isolated-window',
|
||||||
|
}))
|
||||||
|
const mockSpawnTmuxSession = mock<(
|
||||||
|
sessionId: string,
|
||||||
|
description: string,
|
||||||
|
config: TmuxConfig,
|
||||||
|
serverUrl: string,
|
||||||
|
sourcePaneId?: string
|
||||||
|
) => Promise<SpawnTmuxContainerResult>>(async () => ({
|
||||||
|
success: true,
|
||||||
|
paneId: '%isolated-session',
|
||||||
|
}))
|
||||||
const mockIsInsideTmux = mock<() => boolean>(() => true)
|
const mockIsInsideTmux = mock<() => boolean>(() => true)
|
||||||
const mockGetCurrentPaneId = mock<() => string | undefined>(() => '%0')
|
const mockGetCurrentPaneId = mock<() => string | undefined>(() => '%0')
|
||||||
|
|
||||||
@@ -70,6 +94,8 @@ mock.module('../../shared/tmux', () => {
|
|||||||
SESSION_MISSING_GRACE_MS,
|
SESSION_MISSING_GRACE_MS,
|
||||||
SESSION_READY_POLL_INTERVAL_MS: 100,
|
SESSION_READY_POLL_INTERVAL_MS: 100,
|
||||||
SESSION_READY_TIMEOUT_MS: 500,
|
SESSION_READY_TIMEOUT_MS: 500,
|
||||||
|
spawnTmuxWindow: mockSpawnTmuxWindow,
|
||||||
|
spawnTmuxSession: mockSpawnTmuxSession,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,6 +159,8 @@ describe('TmuxSessionManager', () => {
|
|||||||
mockPaneExists.mockClear()
|
mockPaneExists.mockClear()
|
||||||
mockExecuteActions.mockClear()
|
mockExecuteActions.mockClear()
|
||||||
mockExecuteAction.mockClear()
|
mockExecuteAction.mockClear()
|
||||||
|
mockSpawnTmuxWindow.mockClear()
|
||||||
|
mockSpawnTmuxSession.mockClear()
|
||||||
mockIsInsideTmux.mockClear()
|
mockIsInsideTmux.mockClear()
|
||||||
mockGetCurrentPaneId.mockClear()
|
mockGetCurrentPaneId.mockClear()
|
||||||
trackedSessions.clear()
|
trackedSessions.clear()
|
||||||
@@ -150,6 +178,20 @@ describe('TmuxSessionManager', () => {
|
|||||||
results: [],
|
results: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mockSpawnTmuxWindow.mockImplementation(async (sessionId) => {
|
||||||
|
trackedSessions.add(sessionId)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
paneId: `%isolated-window-${sessionId}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mockSpawnTmuxSession.mockImplementation(async (sessionId) => {
|
||||||
|
trackedSessions.add(sessionId)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
paneId: `%isolated-session-${sessionId}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
@@ -349,6 +391,71 @@ describe('TmuxSessionManager', () => {
|
|||||||
expect(actionsArg[0].type).toBe('spawn')
|
expect(actionsArg[0].type).toBe('spawn')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('#given session isolation with healthy existing container #when second subagent is created #then it spawns inline from isolated pane', async () => {
|
||||||
|
// given
|
||||||
|
mockIsInsideTmux.mockReturnValue(true)
|
||||||
|
mockQueryWindowState.mockImplementation(async (paneId) => {
|
||||||
|
if (paneId === '%isolated-session-ses_first') {
|
||||||
|
return createWindowState({
|
||||||
|
mainPane: {
|
||||||
|
paneId,
|
||||||
|
width: 110,
|
||||||
|
height: 44,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
title: 'isolated',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return createWindowState()
|
||||||
|
})
|
||||||
|
|
||||||
|
const { TmuxSessionManager } = await import('./manager')
|
||||||
|
const ctx = createMockContext()
|
||||||
|
const config: TmuxConfig = {
|
||||||
|
enabled: true,
|
||||||
|
isolation: 'session',
|
||||||
|
layout: 'main-vertical',
|
||||||
|
main_pane_size: 60,
|
||||||
|
main_pane_min_width: 80,
|
||||||
|
agent_pane_min_width: 40,
|
||||||
|
}
|
||||||
|
const manager = new TmuxSessionManager(ctx, config, mockTmuxDeps)
|
||||||
|
|
||||||
|
await manager.onSessionCreated(
|
||||||
|
createSessionCreatedEvent('ses_first', 'ses_parent', 'First Task')
|
||||||
|
)
|
||||||
|
|
||||||
|
mockExecuteActions.mockClear()
|
||||||
|
|
||||||
|
// when
|
||||||
|
await manager.onSessionCreated(
|
||||||
|
createSessionCreatedEvent('ses_second', 'ses_parent', 'Second Task')
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(mockSpawnTmuxSession).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockExecuteActions).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
const executeActionsCall = mockExecuteActions.mock.calls[0]
|
||||||
|
expect(executeActionsCall).toBeDefined()
|
||||||
|
const actions = executeActionsCall?.[0]
|
||||||
|
const context = executeActionsCall?.[1]
|
||||||
|
|
||||||
|
expect(actions).toBeDefined()
|
||||||
|
expect(actions).toHaveLength(1)
|
||||||
|
expect(actions?.[0]?.type).toBe('spawn')
|
||||||
|
|
||||||
|
if (actions?.[0]?.type === 'spawn') {
|
||||||
|
expect(actions[0].sessionId).toBe('ses_second')
|
||||||
|
expect(actions[0].targetPaneId).toBe('%isolated-session-ses_first')
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(context?.sourcePaneId).toBe('%isolated-session-ses_first')
|
||||||
|
})
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -537,7 +537,7 @@ export class TmuxSessionManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isIsolated()) {
|
if (this.isIsolated() && !this.isolatedWindowPaneId) {
|
||||||
log("[tmux-session-manager] isolated container failed, skipping inline fallback to preserve isolation", { sessionId })
|
log("[tmux-session-manager] isolated container failed, skipping inline fallback to preserve isolation", { sessionId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user