test(repro): demonstrate #10142 tab X close bypasses running-process confirmation

Unit repro: closeTerminalTab (the X-button/middle-click entry) never consults inspectRuntimeTerminalProcess and drops a tab with a live child.
E2E repro: Cmd+W shows 'Stop running command?' for a tab running sleep 300; cancelling then clicking the tab X closes it silently.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-08-02 21:36:28 -07:00
co-authored by Orca
parent f3e087ec06
commit a92d8e0b0d
2 changed files with 226 additions and 0 deletions
@@ -0,0 +1,132 @@
/**
* Repro for #10142: tab X button / middle-click bypass the running-process close
* confirmation that Cmd+W enforces.
*
* Mouse close path: SortableTab (X onClick / onAuxClick button===1) -> onClose
* -> Terminal.tsx handleCloseTab -> closeTerminalTab() — no process probe.
* Keyboard path: Cmd+W -> TerminalPane.handleRequestClosePane -> inspectRuntimeTerminalProcess
* -> CloseTerminalDialog.
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
requestPinnedTabCloseConfirmMock,
getStateMock,
inspectRuntimeTerminalProcessMock,
isWebRuntimeSessionActiveMock,
resolveHostSessionTabIdForWebSessionTabMock
} = vi.hoisted(() => ({
requestPinnedTabCloseConfirmMock: vi.fn(),
getStateMock: vi.fn(),
inspectRuntimeTerminalProcessMock: vi.fn(),
isWebRuntimeSessionActiveMock: vi.fn(() => false),
resolveHostSessionTabIdForWebSessionTabMock: vi.fn<() => string | null>(() => null)
}))
vi.mock('@/store', () => ({
useAppStore: { getState: getStateMock }
}))
vi.mock('@/runtime/web-runtime-session', () => ({
activateWebRuntimeSessionTab: vi.fn(),
closeWebRuntimeSessionTab: vi.fn(),
createWebRuntimeSessionTerminal: vi.fn(),
isWebRuntimeSessionActive: isWebRuntimeSessionActiveMock,
isWebTerminalSurfaceTabId: vi.fn(() => false),
toHostSessionTabId: vi.fn((tabId: string) => tabId)
}))
vi.mock('@/runtime/web-session-tabs-sync', () => ({
getLatestWebSessionTabsPublicationEpoch: vi.fn(() => 'epoch-1'),
resolveHostSessionTabIdForWebSessionTab: resolveHostSessionTabIdForWebSessionTabMock
}))
vi.mock('@/runtime/runtime-terminal-inspection', () => ({
inspectRuntimeTerminalProcess: inspectRuntimeTerminalProcessMock
}))
import { closeTerminalTab } from './terminal-tab-actions'
// A non-pinned terminal tab whose PTY has a live child process (e.g. `sleep 300`).
function stateWithBusyTerminalTab(closeTab: () => void): Record<string, unknown> {
return {
settings: { activeRuntimeEnvironmentId: null, confirmClosePinnedTab: true },
tabsByWorktree: { 'wt-1': [{ id: 'tab-busy' }, { id: 'tab-other' }] },
unifiedTabsByWorktree: {
'wt-1': [
{ id: 'tab-busy', entityId: 'tab-busy', contentType: 'terminal', isPinned: false },
{ id: 'tab-other', entityId: 'tab-other', contentType: 'terminal', isPinned: false }
]
},
ptyIdsByTabId: { 'tab-busy': ['pty-busy'] },
terminalLayoutsByTabId: { 'tab-busy': { ptyIdsByLeafId: { leaf: 'pty-busy' } } },
agentStatusByPaneKey: {},
openFiles: [],
browserTabsByWorktree: {},
activeWorktreeId: 'wt-1',
activeTabId: 'tab-busy',
closeTab,
requestPinnedTabCloseConfirm: requestPinnedTabCloseConfirmMock,
setActiveTab: vi.fn(),
setActiveFile: vi.fn(),
setActiveTabType: vi.fn(),
setActiveBrowserTab: vi.fn(),
setActiveWorktree: vi.fn()
}
}
describe('#10142 close confirmation policy is the same for keyboard and mouse', () => {
const closeTab = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
getStateMock.mockReturnValue(stateWithBusyTerminalTab(closeTab))
isWebRuntimeSessionActiveMock.mockReturnValue(false)
resolveHostSessionTabIdForWebSessionTabMock.mockReturnValue(null)
inspectRuntimeTerminalProcessMock.mockResolvedValue({
foregroundProcess: 'sleep',
hasChildProcesses: true
})
})
// Control: the keyboard entry point does probe for running children.
it('keyboard Cmd+W path probes for running child processes before closing', () => {
const source = readFileSync(
join(__dirname, '../terminal-pane/TerminalPane.tsx'),
'utf8'
)
const handler = source.slice(source.indexOf('const handleRequestClosePane'))
expect(handler.slice(0, handler.indexOf('useImperativeHandle'))).toContain(
'inspectRuntimeTerminalProcess'
)
})
// Control: the harness does observe a guard when one exists — pinning blocks the same mouse close.
it('mouse close routes a pinned tab through its confirmation guard', () => {
const state = stateWithBusyTerminalTab(closeTab)
;(state.unifiedTabsByWorktree as Record<string, { isPinned: boolean }[]>)['wt-1']![0]!.isPinned =
true
getStateMock.mockReturnValue(state)
closeTerminalTab('tab-busy')
expect(requestPinnedTabCloseConfirmMock).toHaveBeenCalled()
expect(closeTab).not.toHaveBeenCalled()
})
it('mouse close (X button / middle-click) consults the running-process probe', async () => {
closeTerminalTab('tab-busy')
await Promise.resolve()
expect(inspectRuntimeTerminalProcessMock).toHaveBeenCalled()
})
it('mouse close (X button / middle-click) does not drop a busy tab without confirmation', async () => {
closeTerminalTab('tab-busy')
await Promise.resolve()
expect(closeTab).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,94 @@
/**
* Repro for #10142: the tab X button bypasses the running-process close
* confirmation that Cmd/Ctrl+W enforces on the same tab.
*
* Both halves run against one tab with a live `sleep 300` child:
* 1. Cmd+W -> "Stop running command?" dialog (cancelled, tab survives).
* 2. X click -> expected the same dialog; today the tab just disappears.
*/
import { test, expect } from './helpers/orca-app'
import type { Page } from '@stablyai/playwright-test'
import {
waitForSessionReady,
waitForActiveWorktree,
getActiveTabId,
ensureTerminalVisible
} from './helpers/store'
import {
execInTerminal,
focusActiveTerminalInput,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForPaneCount,
waitForTerminalOutput
} from './helpers/terminal'
const SORTABLE_TAB = '[data-testid="sortable-tab"]'
const SCREENSHOT_DIR = '/tmp/vbb/10142/.repro'
function countRenderedTabs(page: Page): Promise<number> {
return page.locator(SORTABLE_TAB).count()
}
function closeDialogTitle(page: Page) {
return page.getByText(/Stop running command\?|Stop this agent\?/)
}
test.describe.configure({ mode: 'serial' })
test('#10142 tab X button bypasses the Cmd+W running-process confirmation', async ({
orcaPage
}) => {
test.setTimeout(120_000)
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
const hasPaneManager = await waitForActiveTerminalManager(orcaPage, 30_000)
.then(() => true)
.catch(() => false)
test.skip(!hasPaneManager, 'Electron automation never mounted the live TerminalPane manager.')
await waitForPaneCount(orcaPage, 1, 30_000)
const ptyId = await waitForActivePanePtyId(orcaPage)
await execInTerminal(orcaPage, ptyId, 'echo repro-10142-ready')
await waitForTerminalOutput(orcaPage, 'repro-10142-ready', 20_000)
await execInTerminal(orcaPage, ptyId, 'sleep 300')
// Only press close once the PTY actually reports the child; otherwise the
// probe legitimately sees an idle shell and closing is correct.
await expect
.poll(
async () =>
(await orcaPage.evaluate((id) => window.api.pty.inspectProcess(id), ptyId))
.hasChildProcesses,
{ timeout: 20_000, message: 'sleep 300 never registered as a child process' }
)
.toBe(true)
const busyTabId = (await getActiveTabId(orcaPage))!
const busyTab = orcaPage.locator(`${SORTABLE_TAB}[data-tab-id="${busyTabId}"]`).first()
// 1. Keyboard close prompts.
await focusActiveTerminalInput(orcaPage)
await orcaPage.keyboard.press(process.platform === 'darwin' ? 'Meta+w' : 'Control+w')
await expect(closeDialogTitle(orcaPage)).toBeVisible({ timeout: 15_000 })
await orcaPage.screenshot({ path: `${SCREENSHOT_DIR}/10142-1-cmdw-shows-confirm.png` })
await orcaPage.getByRole('button', { name: /^Cancel$/ }).click()
await expect(closeDialogTitle(orcaPage)).toBeHidden()
await expect(busyTab).toBeVisible()
const tabsBefore = await countRenderedTabs(orcaPage)
// 2. Same tab, same running child, mouse close.
await busyTab.hover()
await busyTab.getByRole('button', { name: /^Close tab /i }).click()
await orcaPage.waitForTimeout(1_500)
await orcaPage.screenshot({ path: `${SCREENSHOT_DIR}/10142-2-x-click-no-confirm.png` })
expect(
{
confirmDialogVisible: await closeDialogTitle(orcaPage).isVisible(),
tabStillPresent: (await countRenderedTabs(orcaPage)) === tabsBefore
},
'X-button close must apply the same running-process confirmation as Cmd+W'
).toEqual({ confirmDialogVisible: true, tabStillPresent: true })
})