Files
orca/tests/e2e/terminal-tab-close-restart-persistence.spec.ts
T
JinjingandOrcaWin a40183389b feat: bound direct SSH reconnect fan-out and recovery (#11003)
* docs: design for direct SSH reconnect fan-out

Capture the implementation-ready plan for host-qualified, epoch-fenced
SSH reconnect recovery after two rounds of multi-model LLM counsel review.

* docs: reconcile SSH reconnect fan-out design

* docs: close reconnect design consistency gaps

* feat: implement bounded direct SSH reconnect recovery

* fix: bound direct SSH retry settlement

* fix: harden direct SSH reconnect authority

* fix: preserve split SSH retry ownership

* fix: preserve SSH split continuation authority

* docs: record final SSH reconnect validation

* fix: preserve SSH authority through retained and detached state

* fix: retain SSH authority across delayed split mounts

* fix: close SSH authority recovery gaps

* fix: fence stale SSH transport replacement

* fix: serialize SSH target teardown

* fix: settle SSH teardown failures before reconnect

* fix: retire failed SSH reset sessions

* test: reconcile current main E2E contracts

* fix: close direct SSH reconnect review gaps

* fix: fence stale SSH reconnect side effects

* fix: close final SSH reconnect lifecycle gaps

* test: stabilize current-main reliability gates

* test: prove plugin navigation containment

* test: make plugin navigation oracle authoritative

* test: make plugin navigation oracle deterministic

* ci: allow sharded e2e suite to finish

* test: wait for runtime pane publication

* test: classify pane readiness by error code

* test: select close persistence terminal by tab identity

* docs: mark reconnect implementation validated

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 12:33:17 -07:00

152 lines
5.9 KiB
TypeScript

import { existsSync, readFileSync } from 'node:fs'
import type { ElectronApplication } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { TEST_REPO_PATH_FILE } from './global-setup'
import { waitForActiveTerminalManager, waitForPaneCount } from './helpers/terminal'
import {
ensureTerminalVisible,
getActiveTabId,
getWorktreeTabs,
waitForActiveWorktree,
waitForSessionReady
} from './helpers/store'
import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart'
import { RuntimeClient } from '../../src/cli/runtime/client'
import type {
RuntimeTerminalClose,
RuntimeTerminalListResult,
RuntimeTerminalSplit
} from '../../src/shared/runtime-types'
test.describe.configure({ mode: 'serial' })
test('durable whole-tab close removes a split tab across restart', async (// oxlint-disable-next-line no-empty-pattern -- This lifecycle test owns both Electron launches and intentionally opts out of the default app fixture.
{}, testInfo) => {
const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf8').trim()
if (!repoPath || !existsSync(repoPath)) {
test.skip(true, 'Global setup did not produce a seeded test repo')
return
}
const session = createRestartSession(testInfo)
let firstApp: ElectronApplication | null = null
let secondApp: ElectronApplication | null = null
try {
const firstLaunch = await session.launch()
firstApp = firstLaunch.app
const worktreeId = await attachRepoAndOpenTerminal(firstLaunch.page, repoPath)
await waitForSessionReady(firstLaunch.page)
await waitForActiveWorktree(firstLaunch.page)
await ensureTerminalVisible(firstLaunch.page)
const hasPaneManager = await waitForActiveTerminalManager(firstLaunch.page, 30_000)
.then(() => true)
.catch(() => false)
test.skip(
!hasPaneManager,
'Electron automation in this environment never mounted the TerminalPane manager.'
)
await waitForPaneCount(firstLaunch.page, 1, 30_000)
const closedTabId = await getActiveTabId(firstLaunch.page)
if (!closedTabId) {
throw new Error('First launch did not expose an active terminal tab')
}
expect(await getWorktreeTabs(firstLaunch.page, worktreeId)).toHaveLength(1)
const client = new RuntimeClient(session.userDataDir, 30_000)
let activeHandle: string | null = null
await expect
.poll(
async () => {
const listed = await client.call<RuntimeTerminalListResult>('terminal.list', {
worktree: `id:${worktreeId}`
})
const matching = listed.result.terminals.filter(
(terminal) => terminal.worktreeId === worktreeId && terminal.tabId === closedTabId
)
activeHandle = matching.length === 1 ? (matching[0]?.handle ?? null) : null
return matching.length
},
{ message: 'Closed-tab candidate did not become uniquely runtime-visible' }
)
.toBe(1)
if (!activeHandle) {
throw new Error('Closed-tab candidate became visible without a terminal handle')
}
const split = await client.call<{ split: RuntimeTerminalSplit }>('terminal.split', {
terminal: activeHandle,
direction: 'vertical'
})
expect(split.result.split.tabId).toBe(closedTabId)
await waitForPaneCount(firstLaunch.page, 2, 30_000)
const close = await client.call<{ close: RuntimeTerminalClose }>('terminal.closeTab', {
terminal: split.result.split.handle
})
expect(close.result.close).toMatchObject({
handle: split.result.split.handle,
tabId: closedTabId,
closeMode: 'tab'
})
await expect
.poll(() => getWorktreeTabs(firstLaunch.page, worktreeId), {
message: 'The acknowledged close left the split terminal tab in renderer state'
})
.toEqual([])
await expect
.poll(
async () => {
const afterClose = await client.call<RuntimeTerminalListResult>('terminal.list', {
worktree: `id:${worktreeId}`
})
return afterClose.result.terminals
.filter((terminal) => terminal.tabId === closedTabId)
.map((terminal) => terminal.handle)
},
{ message: 'The acknowledged close left host terminal rows alive' }
)
.toEqual([])
await session.close(firstApp)
firstApp = null
const secondLaunch = await session.launch()
secondApp = secondLaunch.app
await waitForSessionReady(secondLaunch.page)
const restoredWorktreeId = await attachRepoAndOpenTerminal(secondLaunch.page, repoPath)
expect(restoredWorktreeId).toBe(worktreeId)
// Why: wait past initial worktree effects so this checks resurrection, not
// only the first hydrated frame before default-tab logic has run.
await secondLaunch.page.waitForTimeout(1_000)
// Why: reattaching to an emptied worktree intentionally spawns a fresh
// "Terminal 1" tab (Terminal.tsx's shouldAutoCreateInitialTerminal
// fallback fires whenever the active worktree has zero renderable tabs —
// true for a durably-closed worktree just like a brand-new one). That
// fallback is unrelated to this test and reproduces even mid-session with
// no restart at all, so asserting an eternally-empty tab list here is
// wrong. What "durable" actually promises is that the specific closed
// split tab never comes back — assert on its identity, not on tab count.
const restoredTabs = await getWorktreeTabs(secondLaunch.page, worktreeId)
expect(restoredTabs.some((tab) => tab.id === closedTabId)).toBe(false)
const afterRestart = await client.call<RuntimeTerminalListResult>('terminal.list', {
worktree: `id:${worktreeId}`
})
expect(
afterRestart.result.terminals.filter((terminal) => terminal.tabId === closedTabId)
).toEqual([])
} finally {
if (firstApp) {
await session.close(firstApp)
}
if (secondApp) {
await session.close(secondApp)
}
await session.dispose()
}
})