test(e2e): cover session upgrade and Windows terminal recovery (#17289)

* coverage report

* rm test coverage

* test(e2e): cover session upgrade and Windows terminal recovery

* fix stub
This commit is contained in:
Jinjing
2026-08-29 22:37:05 -07:00
committed by GitHub
parent 56874e6006
commit 73ff003147
7 changed files with 583 additions and 18 deletions
+36 -13
View File
@@ -8151,15 +8151,15 @@
"id": "startup-upgrade.persisted-session-corpus",
"title": "Current Orca preserves or recovers old production persisted sessions",
"maturity": "experimental",
"protection": "none",
"protection": "partial",
"owner": "startup-persistence",
"layer": "upgrade-fixture",
"surfaces": ["startup", "upgrade", "session restore", "daemon restore"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "daemon", "ssh", "wsl"],
"coveredPlatforms": [],
"coveredProviders": [],
"coverageNotes": "Registered gap only; no executable coverage is wired yet.",
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "daemon"],
"coverageNotes": "A macOS Electron journey materializes the immutable legacy workspace-session fixture over a live update-surviving daemon PTY, boots current Orca without a current-format session write, and proves the terminal title, marker output, exact PTY, tab identity, repo, and worktree survive both the upgrade launch and a second current-version relaunch. Linux, Windows, SSH, and WSL fixture journeys remain uncollected.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/5356",
"https://github.com/stablyai/orca/pull/5234",
@@ -8167,21 +8167,40 @@
],
"invariant": "Startup and restore fixes must preserve or explicitly recover sessions from the last affected production persisted-state schema, not only from state written by current code.",
"oracle": "Boot current Orca against immutable copied user-data fixtures from affected versions and reject blank replacement panes, duplicate resume tabs, silent session loss, or works-only-after-current-write behavior.",
"commands": [],
"testFiles": [],
"assertionRefs": [],
"evidenceRuns": [],
"commands": [
"SKIP_BUILD=1 pnpm run test:e2e -- tests/e2e/persisted-session-production-upgrade.spec.ts"
],
"testFiles": ["tests/e2e/persisted-session-production-upgrade.spec.ts"],
"assertionRefs": [
{
"file": "tests/e2e/persisted-session-production-upgrade.spec.ts",
"assertions": [
"upgrades a legacy daemon session and keeps it stable after relaunch"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-29",
"runner": "local",
"platform": "macos",
"command": "SKIP_BUILD=1 pnpm run test:e2e -- tests/e2e/persisted-session-production-upgrade.spec.ts",
"result": "passed",
"durationSeconds": 16.7,
"summary": "The legacy-shaped session retained its exact daemon PTY, terminal output marker, tab, repository, and worktree through the upgrade launch and a second current-version relaunch."
}
],
"runtimeBudget": {
"p95Seconds": 120,
"scope": "focused Electron upgrade fixture"
},
"flakeHistory": {
"status": "not-started",
"evidence": "Fixture corpus not implemented."
"status": "unknown",
"evidence": "One focused local macOS Electron run passed; CI soak history is not collected."
},
"redGreenEvidence": {
"status": "missing",
"evidence": "Needs immutable pre-fix persisted-state fixture."
"status": "partial",
"evidence": "The immutable legacy-shaped fixture and first-write/second-restart oracle are executable; saved intentional-break red/green evidence is still required."
},
"performanceBudget": {
"required": true,
@@ -8192,7 +8211,11 @@
"Run second restart after current code writes upgraded state.",
"Record startup timing and failure artifact."
],
"knownGaps": ["No fixture corpus or command yet."],
"knownGaps": [
"The corpus currently contains one minimized legacy local-daemon workspace session.",
"Linux, Windows, SSH, and WSL upgrade fixtures remain uncollected.",
"The setup uses a current binary to create the live daemon PTY that stands in for the pre-upgrade process; it does not launch an installed legacy artifact."
],
"demotionRule": "Cannot promote without old production fixture provenance."
},
{
@@ -4,6 +4,7 @@ const READY_MARKER = 'GOLDEN_STUB_AGENT_READY'
const EXIT_MARKER = 'GOLDEN_STUB_AGENT_EXITED'
const ESC = '\x1b'
const keyboardProtocolMode = process.argv.includes('--keyboard-protocol')
// Both match the bytes after ESC, so the control character stays out of the
// pattern: a CSI/SS3 introducer still missing its final byte, and a complete
// CSI/SS3 sequence. Shift+Enter is matched before either is consulted.
@@ -19,7 +20,7 @@ function render() {
const lines = composer.split('\n')
const renderedComposer = lines.map((line, index) => `${index === 0 ? '> ' : ' '}${line}`)
process.stdout.write(
`\x1b]0;Golden Stub Agent\x07${[
`${keyboardProtocolMode ? '\x1b]0;\u280b Codex is thinking\x07\x1b[>1u' : '\x1b]0;Golden Stub Agent\x07'}${[
'\x1b[H\x1b[2JGolden Stub Agent',
`[${READY_MARKER}]`,
'',
@@ -39,7 +40,8 @@ function exitCleanly() {
if (process.stdin.isTTY) {
process.stdin.setRawMode(false)
}
process.stdout.write(`\x1b[?1049l[${EXIT_MARKER}]\r\n`, () => process.exit(0))
const idleTitle = keyboardProtocolMode ? '\x1b]0;Codex\x07' : ''
process.stdout.write(`${idleTitle}\x1b[?1049l[${EXIT_MARKER}]\r\n`, () => process.exit(0))
}
function submit() {
@@ -54,6 +56,12 @@ function submit() {
function consumeInput() {
while (pendingInput.length > 0 && !exiting) {
const enter = ['\x1b[13u', '\x1b[13;1u'].find((sequence) => pendingInput.startsWith(sequence))
if (enter) {
pendingInput = pendingInput.slice(enter.length)
submit()
continue
}
const shiftEnter = ['\x1b[13;2u', '\x1b[13;2~', '\x1b\r'].find((sequence) =>
pendingInput.startsWith(sequence)
)
@@ -0,0 +1,48 @@
{
"_fixtureProvenance": {
"description": "Legacy Orca workspace session with one daemon-backed terminal tab, used to verify upgrade and relaunch recovery.",
"sourceVersion": "legacy production workspaceSession shape",
"minimizedAt": "2026-07-01",
"notes": [
"The pre-fix shape intentionally omits sleepingAgentSessionsByPaneKey.",
"Legacy tab and layout PTY ids are materialized from the update-surviving daemon session."
]
},
"activeRepoId": "__REPO_ID__",
"activeWorktreeId": "__WORKTREE_ID__",
"activeTabId": "production-agent-tab",
"tabsByWorktree": {
"__WORKTREE_ID__": [
{
"id": "production-agent-tab",
"ptyId": "__PTY_ID__",
"worktreeId": "__WORKTREE_ID__",
"title": "Production agent session",
"defaultTitle": "Terminal 1",
"generatedTitle": "Production agent session",
"customTitle": "Production agent session",
"color": null,
"sortOrder": 0,
"createdAt": 1717444800000,
"launchAgent": "codex"
}
]
},
"terminalLayoutsByTabId": {
"production-agent-tab": {
"root": { "type": "leaf", "leafId": "pane:production-agent" },
"activeLeafId": "pane:production-agent",
"expandedLeafId": null,
"ptyIdsByLeafId": {
"pane:production-agent": "__PTY_ID__"
},
"titlesByLeafId": {
"pane:production-agent": "Production agent session"
}
}
},
"activeWorktreeIdsOnShutdown": ["__WORKTREE_ID__"],
"activeTabIdByWorktree": {
"__WORKTREE_ID__": "production-agent-tab"
}
}
+4 -3
View File
@@ -26,13 +26,14 @@ export async function configureGoldenStubAgent(
page: Page,
options: {
agent?: (typeof GOLDEN_STUB_AGENTS)[number]['id']
agentArgs?: string
/** Windows default shell the launch command must survive; ignored elsewhere. */
windowsShell?: BuiltInWindowsTerminalShell
} = {}
): Promise<void> {
const agent = options.agent ?? 'codex'
await page.evaluate(
async ({ agent, windowsShell }) => {
async ({ agent, agentArgs, windowsShell }) => {
const store = window.__store
if (!store) {
throw new Error('Orca store is unavailable')
@@ -40,11 +41,11 @@ export async function configureGoldenStubAgent(
await store.getState().updateSettings({
defaultTuiAgent: agent,
agentCmdOverrides: { [agent]: 'golden-stub-agent' },
agentDefaultArgs: { [agent]: '' },
agentDefaultArgs: { [agent]: agentArgs },
...(windowsShell ? { terminalWindowsShell: windowsShell } : {})
})
},
{ agent, windowsShell: options.windowsShell ?? null }
{ agent, agentArgs: options.agentArgs ?? '', windowsShell: options.windowsShell ?? null }
)
}
@@ -0,0 +1,171 @@
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { DEFAULT_LOCAL_ORCA_PROFILE_ID } from '../../src/shared/orca-profiles'
import { PTY_SESSION_ID_SEPARATOR } from '../../src/shared/pty-session-id-format'
import { test, expect } from './helpers/orca-app'
import { TEST_REPO_PATH_FILE } from './global-setup'
import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart'
import { ensureTerminalVisible, waitForSessionReady } from './helpers/store'
import {
discoverActivePtyId,
execInTerminal,
getTerminalContent,
waitForActiveTerminalManager,
waitForPaneCount,
waitForTerminalOutput
} from './helpers/terminal'
const FIXTURE_PATH = path.join(
process.cwd(),
'tests',
'e2e',
'fixtures',
'persisted-sessions',
'legacy-workspace-session-daemon-terminal.json'
)
// This fixture captures a legacy production schema boundary; the test runs the current build.
const RESTORED_TITLE = 'Production agent session'
type FixtureSession = {
_fixtureProvenance?: unknown
activeRepoId: string
activeWorktreeId: string
tabsByWorktree: Record<string, { ptyId: string; worktreeId: string }[]>
terminalLayoutsByTabId: Record<string, { ptyIdsByLeafId?: Record<string, string> }>
activeWorktreeIdsOnShutdown?: string[]
activeTabIdByWorktree?: Record<string, string>
}
function installProductionSessionFixture(
userDataDir: string,
repoId: string,
worktreeId: string,
ptyId: string
): void {
const profilePath = path.join(
userDataDir,
'profiles',
DEFAULT_LOCAL_ORCA_PROFILE_ID,
'orca-data.json'
)
const profile = JSON.parse(readFileSync(profilePath, 'utf8')) as Record<string, unknown>
const fixture = JSON.parse(readFileSync(FIXTURE_PATH, 'utf8')) as FixtureSession
const fixtureTabs = fixture.tabsByWorktree.__WORKTREE_ID__
const fixtureLayout = fixture.terminalLayoutsByTabId['production-agent-tab']
if (!fixtureTabs || !fixtureLayout) {
throw new Error('Production session fixture is missing its terminal records')
}
delete fixture._fixtureProvenance
fixture.activeRepoId = repoId
fixture.activeWorktreeId = worktreeId
fixture.tabsByWorktree = {
[worktreeId]: fixtureTabs.map((tab) => ({
...tab,
ptyId,
worktreeId
}))
}
fixtureLayout.ptyIdsByLeafId = {
'pane:production-agent': ptyId
}
fixture.activeWorktreeIdsOnShutdown = [worktreeId]
fixture.activeTabIdByWorktree = { [worktreeId]: 'production-agent-tab' }
profile.workspaceSession = fixture
writeFileSync(profilePath, `${JSON.stringify(profile, null, 2)}\n`)
}
async function expectProductionSessionRestored(
page: Page,
expected: { marker: string; ptyId: string; repoId: string; worktreeId: string }
): Promise<void> {
await waitForSessionReady(page)
await ensureTerminalVisible(page)
await waitForActiveTerminalManager(page, 30_000)
await waitForPaneCount(page, 1, 30_000)
await waitForTerminalOutput(page, expected.marker, 30_000)
await expect(
page.locator('[data-testid="sortable-tab"]').filter({ hasText: RESTORED_TITLE })
).toBeVisible({ timeout: 15_000 })
await expect(page.locator('[data-testid="sortable-tab"]')).toHaveCount(1)
await expect(page.locator('.xterm').first()).toBeVisible()
expect(await discoverActivePtyId(page)).toBe(expected.ptyId)
expect(await getTerminalContent(page)).toContain(expected.marker)
expect(
await page.evaluate(() => {
const state = window.__store?.getState()
return {
activeRepoId: state?.activeRepoId,
activeWorktreeId: state?.activeWorktreeId,
tabIds: state?.activeWorktreeId
? state.tabsByWorktree[state.activeWorktreeId]?.map((tab) => tab.id)
: []
}
})
).toEqual({
activeRepoId: expected.repoId,
activeWorktreeId: expected.worktreeId,
tabIds: ['production-agent-tab']
})
}
test('upgrades a legacy daemon session and keeps it stable after relaunch', async (// oxlint-disable-next-line no-empty-pattern -- this upgrade test owns its Electron launches.
{}, testInfo) => {
test.setTimeout(300_000)
const repoPath = existsSync(TEST_REPO_PATH_FILE)
? readFileSync(TEST_REPO_PATH_FILE, 'utf8').trim()
: ''
test.skip(!repoPath || !existsSync(repoPath), 'Seeded E2E repository is unavailable')
const session = createRestartSession(testInfo)
let oldApp: ElectronApplication | null = null
let currentApp: ElectronApplication | null = null
let stableApp: ElectronApplication | null = null
try {
const oldLaunch = await session.launch()
oldApp = oldLaunch.app
const worktreeId = await attachRepoAndOpenTerminal(oldLaunch.page, repoPath)
await waitForSessionReady(oldLaunch.page)
await ensureTerminalVisible(oldLaunch.page)
await waitForActiveTerminalManager(oldLaunch.page, 30_000)
await waitForPaneCount(oldLaunch.page, 1, 30_000)
const ptyId = await discoverActivePtyId(oldLaunch.page)
expect(ptyId).toContain(PTY_SESSION_ID_SEPARATOR)
const marker = `PRODUCTION_UPGRADE_${Date.now()}`
await execInTerminal(oldLaunch.page, ptyId, `echo ${marker}`)
await waitForTerminalOutput(oldLaunch.page, marker)
const repoId = await oldLaunch.page.evaluate(
(repoPath) => window.__store?.getState().repos.find((repo) => repo.path === repoPath)?.id,
repoPath
)
if (!repoId) {
throw new Error('Active repository was unavailable before fixture installation')
}
await session.close(oldApp)
oldApp = null
installProductionSessionFixture(session.userDataDir, repoId, worktreeId, ptyId)
const currentLaunch = await session.launch()
currentApp = currentLaunch.app
const expected = { marker, ptyId, repoId, worktreeId }
await expectProductionSessionRestored(currentLaunch.page, expected)
await session.close(currentApp)
currentApp = null
const stableLaunch = await session.launch()
stableApp = stableLaunch.app
await expectProductionSessionRestored(stableLaunch.page, expected)
} finally {
for (const app of [stableApp, currentApp, oldApp]) {
if (app) {
await session.close(app).catch(() => {})
}
}
await session.dispose()
}
})
@@ -0,0 +1,102 @@
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import {
configureGoldenStubAgent,
getGoldenStubAgentLaunchEnv,
GOLDEN_STUB_EXIT_MARKER,
launchGoldenStubAgentFromNewTab
} from './helpers/golden-stub-agent'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
focusActiveTerminalInput,
waitForActivePanePtyId,
waitForTerminalOutput
} from './helpers/terminal'
import {
clearTerminalPtyWriteLog,
installTerminalPtyWriteSpy,
readTerminalPtyWriteEntries
} from './helpers/terminal-pty-write-spy'
test.use({ launchEnv: getGoldenStubAgentLaunchEnv() })
test.skip(process.platform !== 'win32', 'A real Windows ConPTY is required')
async function getKittyKeyboardFlags(page: Page): Promise<number | null> {
return page.evaluate(() => {
const state = window.__store?.getState()
const worktreeId = state?.activeWorktreeId
const tabId =
state?.activeTabType === 'terminal'
? state.activeTabId
: worktreeId
? (state?.activeTabIdByWorktree?.[worktreeId] ?? null)
: null
const pane = tabId ? window.__paneManagers?.get(tabId)?.getActivePane?.() : null
const terminal = pane?.terminal as
| {
core?: { coreService?: { kittyKeyboard?: { flags?: number } } }
_core?: { coreService?: { kittyKeyboard?: { flags?: number } } }
}
| undefined
return (
terminal?.core?.coreService?.kittyKeyboard?.flags ??
terminal?._core?.coreService?.kittyKeyboard?.flags ??
null
)
})
}
test('resets standard keyboard bytes after a protocol-mode agent exits on ConPTY', async ({
electronApp,
orcaPage
}) => {
await installTerminalPtyWriteSpy(electronApp)
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)
await configureGoldenStubAgent(orcaPage, { agentArgs: '--keyboard-protocol' })
await launchGoldenStubAgentFromNewTab(orcaPage)
const ptyId = await waitForActivePanePtyId(orcaPage)
await expect.poll(() => getKittyKeyboardFlags(orcaPage), { timeout: 10_000 }).toBe(1)
await clearTerminalPtyWriteLog(electronApp)
await orcaPage.keyboard.type('exit')
await orcaPage.keyboard.press('Enter')
await waitForTerminalOutput(orcaPage, GOLDEN_STUB_EXIT_MARKER, 15_000)
const protocolWrites = (await readTerminalPtyWriteEntries(electronApp))
.filter((entry) => entry.id === ptyId)
.map((entry) => entry.data)
.join('')
expect(protocolWrites.includes('\x1b[13u') || protocolWrites.includes('\x1b[13;1u')).toBe(true)
await expect.poll(() => getKittyKeyboardFlags(orcaPage), { timeout: 10_000 }).toBe(0)
await clearTerminalPtyWriteLog(electronApp)
await focusActiveTerminalInput(orcaPage)
await orcaPage.keyboard.type("Write-Output ('CONPTY_KEYBOARD_' + '")
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), 'REET_')
await orcaPage.keyboard.press('Control+V')
await orcaPage.keyboard.press('ArrowLeft')
await orcaPage.keyboard.press('ArrowLeft')
await orcaPage.keyboard.press('ArrowLeft')
await orcaPage.keyboard.type('S')
await orcaPage.keyboard.press('ArrowRight')
await orcaPage.keyboard.press('ArrowRight')
await orcaPage.keyboard.press('ArrowRight')
await orcaPage.keyboard.type('EXECUTEX')
await orcaPage.keyboard.press('Backspace')
await orcaPage.keyboard.type("D')")
await orcaPage.keyboard.press('Enter')
await waitForTerminalOutput(orcaPage, 'CONPTY_KEYBOARD_RESET_EXECUTED', 15_000)
const shellWrites = (await readTerminalPtyWriteEntries(electronApp))
.filter((entry) => entry.id === ptyId)
.map((entry) => entry.data)
const joinedShellWrites = shellWrites.join('')
expect(joinedShellWrites).toContain('REET_')
expect(shellWrites.filter((data) => data === '\x1b[D')).toHaveLength(3)
expect(shellWrites.filter((data) => data === '\x1b[C')).toHaveLength(3)
expect(shellWrites).toContain('\x7f')
expect(shellWrites).toContain('\r')
expect(joinedShellWrites).not.toMatch(new RegExp(`${String.fromCharCode(27)}\\[\\d+(?:;\\d+)*u`))
})
@@ -0,0 +1,212 @@
import { randomUUID } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { buildWslExecArgs } from '../../src/shared/wsl-login-shell-command'
import { test, expect } from './helpers/orca-app'
import { TEST_REPO_PATH_FILE } from './global-setup'
import { attachRepoAndOpenTerminal, createRestartSession } from './helpers/orca-restart'
import { useWslRuntimeForActiveProject as selectWslRuntimeForActiveProject } from './helpers/wsl-golden-stub-agent'
import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store'
import {
execInTerminal,
getTerminalContent,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForTerminalOutput
} from './helpers/terminal'
const REQUIRE_WSL_RESTORE = process.env.ORCA_REQUIRE_WSL_RESTORE_E2E === '1'
type WslRestoreSnapshot = {
hostCwd: string
ptyId: string
sessionIds: string[]
shellOverride: string | undefined
tabIds: string[]
}
function getGuestPath(distro: string, hostPath: string): string {
return execFileSync('wsl.exe', buildWslExecArgs(distro, ['wslpath', '-a', hostPath]), {
encoding: 'utf8',
stdio: 'pipe',
timeout: 15_000,
windowsHide: true
}).trim()
}
async function createWslStartupTab(
page: Page,
worktreeId: string,
distro: string,
marker: string
): Promise<string> {
await selectWslRuntimeForActiveProject(page, distro)
return page.evaluate(
({ worktreeId, marker }) => {
const store = window.__store
if (!store) {
throw new Error('Store unavailable')
}
const tab = store.getState().createTab(worktreeId, undefined, undefined, {
pendingStartup: {
command: `printf '${marker}:%s\\n' "$PWD"`,
delivery: 'terminal-paste'
}
})
store.getState().setActiveTab(tab.id)
store.getState().setActiveTabType('terminal')
return tab.id
},
{ worktreeId, marker }
)
}
async function readSnapshot(
page: Page,
worktreeId: string,
tabId: string
): Promise<WslRestoreSnapshot> {
return page.evaluate(
async ({ worktreeId, tabId }) => {
const state = window.__store?.getState()
if (!state) {
throw new Error('Store unavailable')
}
const tab = (state.tabsByWorktree[worktreeId] ?? []).find(
(candidate) => candidate.id === tabId
)
const ptyIds = state.ptyIdsByTabId[tabId] ?? []
if (!tab || ptyIds.length !== 1) {
throw new Error(`Expected one PTY for restored WSL tab ${tabId}`)
}
const sessions = await window.api.pty.listSessions()
const ownedSession = sessions.find((session) => session.id === ptyIds[0])
if (!ownedSession) {
throw new Error(`WSL PTY ${ptyIds[0]} was absent from provider inventory`)
}
return {
hostCwd: ownedSession.cwd,
ptyId: ptyIds[0]!,
sessionIds: sessions.map((session) => session.id).sort(),
shellOverride: tab.shellOverride,
tabIds: (state.tabsByWorktree[worktreeId] ?? []).map((candidate) => candidate.id).sort()
}
},
{ worktreeId, tabId }
)
}
async function waitForRestoredWslTab(page: Page, worktreeId: string, tabId: string): Promise<void> {
await waitForSessionReady(page, 60_000)
await expect.poll(() => waitForActiveWorktree(page), { timeout: 60_000 }).toBe(worktreeId)
await ensureTerminalVisible(page)
await page.evaluate((tabId) => {
const state = window.__store?.getState()
state?.setActiveTab(tabId)
state?.setActiveTabType('terminal')
}, tabId)
await waitForActiveTerminalManager(page, 60_000)
await waitForActivePanePtyId(page, 60_000)
}
test('WSL terminal preserves guest cwd, liveness, and PTY ownership across relaunch', async (// oxlint-disable-next-line no-empty-pattern -- This restart test owns both Electron launches.
{}, testInfo) => {
test.skip(process.platform !== 'win32', 'WSL restore coverage requires a Windows host')
test.setTimeout(300_000)
const repoPath = existsSync(TEST_REPO_PATH_FILE)
? readFileSync(TEST_REPO_PATH_FILE, 'utf8').trim()
: ''
if (!repoPath || !existsSync(repoPath)) {
if (REQUIRE_WSL_RESTORE) {
throw new Error('Required WSL restore E2E seeded repo is unavailable')
}
test.skip(true, 'Global setup did not produce a seeded test repo')
return
}
const restart = createRestartSession(testInfo)
let app: ElectronApplication | null = null
try {
const first = await restart.launch()
app = first.app
const worktreeId = await attachRepoAndOpenTerminal(first.page, repoPath)
await waitForSessionReady(first.page)
await waitForActiveWorktree(first.page)
await ensureTerminalVisible(first.page)
const distro = await first.page.evaluate(async () => {
if (!(await window.api.wsl.isAvailable())) {
return null
}
return (await window.api.wsl.listDistros())[0] ?? null
})
if (!distro) {
if (REQUIRE_WSL_RESTORE) {
throw new Error('Required Windows WSL distro is unavailable')
}
test.skip(true, 'No WSL distro is available on this Windows host')
return
}
const guestPath = getGuestPath(distro, repoPath)
const startupMarker = `WSL_RESTORE_STARTUP_${randomUUID().replaceAll('-', '_')}`
const tabId = await createWslStartupTab(first.page, worktreeId, distro, startupMarker)
await waitForActiveTerminalManager(first.page, 60_000)
const firstPtyId = await waitForActivePanePtyId(first.page, 60_000)
await waitForTerminalOutput(first.page, `${startupMarker}:${guestPath}`, 30_000)
const baseline = await readSnapshot(first.page, worktreeId, tabId)
expect(baseline.hostCwd).toBe(repoPath)
expect(baseline.ptyId).toBe(firstPtyId)
expect(baseline.shellOverride).toBe('wsl.exe')
await expect(
first.page.evaluate((ptyId) => window.api.pty.hasPty(ptyId), firstPtyId)
).resolves.toBe(true)
await first.page.evaluate(() => window.dispatchEvent(new Event('beforeunload')))
await expect
.poll(
() =>
first.page.evaluate(
async ({ worktreeId, tabId, ptyId }) => {
const persisted = await window.api.session.get()
const tab = (persisted.tabsByWorktree[worktreeId] ?? []).find(
(candidate) => candidate.id === tabId
)
const layoutPtyId = persisted.terminalLayoutsByTabId?.[tabId]?.ptyIdsByLeafId
? Object.values(persisted.terminalLayoutsByTabId[tabId]!.ptyIdsByLeafId!)[0]
: undefined
return tab?.id === tabId && layoutPtyId === ptyId
},
{ worktreeId, tabId, ptyId: firstPtyId }
),
{ timeout: 15_000, message: 'WSL tab ownership was not persisted before quit' }
)
.toBe(true)
await restart.close(app)
app = null
const second = await restart.launch()
app = second.app
await waitForRestoredWslTab(second.page, worktreeId, tabId)
await waitForTerminalOutput(second.page, `${startupMarker}:${guestPath}`, 30_000)
const restored = await readSnapshot(second.page, worktreeId, tabId)
expect(restored).toEqual(baseline)
await expect(
second.page.evaluate((ptyId) => window.api.pty.hasPty(ptyId), restored.ptyId)
).resolves.toBe(true)
const inputMarker = `WSL_RESTORE_INPUT_${randomUUID().replaceAll('-', '_')}`
await execInTerminal(second.page, restored.ptyId, `printf '${inputMarker}:%s\\n' "$PWD"`)
await waitForTerminalOutput(second.page, `${inputMarker}:${guestPath}`, 30_000)
expect(await getTerminalContent(second.page)).toContain(`${inputMarker}:${guestPath}`)
} finally {
if (app) {
await restart.close(app)
}
await restart.dispose()
}
})