mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
* 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>
152 lines
5.9 KiB
TypeScript
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()
|
|
}
|
|
})
|