mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test(layout): a serve restart that lost a pane's binding shows the terminal ready at once in the layout-owner build (#31132)
In the layout-owner build, main's load binds the pane again from its tab row while the terminal is alive, so the replacement host publishes the pane ready and never pending. The serve-restart spec waited for a pending frame that this build correctly never sends. It now expects pending only in the current build, and asserts no pending frame in the layout-owner build. A unit test pins the precondition on a windowless host: main's own writes leave no shutdown list, and a real store load restores the binding. Co-authored-by: E2E Test <e2e@test.local>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
|
||||
import type { Store } from '../loading-store/store'
|
||||
import { leafIds } from './terminal-topology-window-session-fixture'
|
||||
import {
|
||||
emptyTerminalSessionProfile,
|
||||
FIXTURE_GIT_WORKTREE_ID,
|
||||
openTopologyStore,
|
||||
reopenTopologyStore
|
||||
} from './terminal-topology-profile-fixture'
|
||||
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
|
||||
vi.mock('../../ssh/config/ssh-config-parser', () => ({
|
||||
loadUserSshConfig: () => ({ hosts: [] }),
|
||||
sshConfigHostsToTargets: () => []
|
||||
}))
|
||||
|
||||
const WORKTREE = FIXTURE_GIT_WORKTREE_ID
|
||||
const TAB = 'tab-cli'
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
const PTY = 'pty-live'
|
||||
const directories: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
/** What a serve host with no window stores for a client-created terminal, as main writes it. */
|
||||
async function windowlessHostWithTerminal(): Promise<{ store: Store; directory: string }> {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-windowless-restart-'))
|
||||
directories.push(directory)
|
||||
const store = await openTopologyStore(directory, emptyTerminalSessionProfile())
|
||||
const created = await store.applyLayoutCommand({
|
||||
type: 'createTerminalTab',
|
||||
workspace: WORKTREE,
|
||||
tabId: TAB,
|
||||
leafId: LEAF
|
||||
})
|
||||
expect(created.ok).toBe(true)
|
||||
await store.persistPtyBinding({
|
||||
worktreeId: WORKTREE,
|
||||
tabId: TAB,
|
||||
leafId: LEAF,
|
||||
ptyId: PTY,
|
||||
incarnationId: 'inc-1'
|
||||
})
|
||||
await store.flushPendingOrThrowAsync()
|
||||
return { store, directory }
|
||||
}
|
||||
|
||||
/** The reported loss: the pane's binding is gone; the tab, its pane tree and the PTY stay. */
|
||||
function withoutPaneBinding(session: WorkspaceSessionState): WorkspaceSessionState {
|
||||
const layout = session.terminalLayoutsByTabId[TAB]!
|
||||
return {
|
||||
...session,
|
||||
terminalLayoutsByTabId: {
|
||||
...session.terminalLayoutsByTabId,
|
||||
[TAB]: { ...layout, ptyIdsByLeafId: {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('a windowless host restarted after a pane lost its terminal binding (layout-owner build)', () => {
|
||||
it('keeps the tab and binds its pane again from the tab row', async () => {
|
||||
const { store, directory } = await windowlessHostWithTerminal()
|
||||
const stored = store.getWorkspaceSession()
|
||||
expect(stored.activeWorktreeIdsOnShutdown).toBeUndefined()
|
||||
store.setWorkspaceSession(withoutPaneBinding(stored))
|
||||
|
||||
const relaunched = await reopenTopologyStore(store, directory)
|
||||
const session = relaunched.getWorkspaceSession()
|
||||
|
||||
expect(session.tabsByWorktree[WORKTREE]?.map((row) => row.id)).toEqual([TAB])
|
||||
expect(leafIds(session.terminalLayoutsByTabId[TAB]?.root)).toEqual([LEAF])
|
||||
expect(session.terminalLayoutsByTabId[TAB]?.ptyIdsByLeafId).toEqual({ [LEAF]: PTY })
|
||||
})
|
||||
})
|
||||
@@ -47,6 +47,9 @@ import {
|
||||
const scratch = createRetentionFixtureDirectory()
|
||||
const fixturePath = path.join(scratch, 'serve-restart-binding-terminal.mjs')
|
||||
const sinkPath = path.join(scratch, 'serve-restart-binding-terminal.log')
|
||||
const LAYOUT_OWNER_BUILD = existsSync(
|
||||
path.join(process.cwd(), 'out', 'main', 'ORCA_LAYOUT_OWNER_MAIN')
|
||||
)
|
||||
|
||||
writeFileSync(
|
||||
fixturePath,
|
||||
@@ -634,29 +637,33 @@ test('retains a verified mirrored PTY binding through a serve restart pending sn
|
||||
expect(host.app.process().pid, 'The serve Electron process must actually be replaced').not.toBe(
|
||||
hostPidBeforeRestart
|
||||
)
|
||||
let pendingReceipt: PairedTerminalSnapshotReceipt | null = null
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
pendingReceipt =
|
||||
(await readPairedTerminalSnapshotProbe(client!.page)).receipts.find(
|
||||
(receipt) => receipt.status === 'pending-handle'
|
||||
) ?? null
|
||||
return pendingReceipt
|
||||
},
|
||||
{
|
||||
timeout: 60_000,
|
||||
message: 'Paired renderer never received the replacement host pending-handle frame'
|
||||
}
|
||||
)
|
||||
.not.toBeNull()
|
||||
expect(pendingReceipt).toMatchObject({
|
||||
leafId: terminal.leafId,
|
||||
parentTabId: terminal.parentTabId,
|
||||
ptyId: terminal.ptyId,
|
||||
status: 'pending-handle',
|
||||
terminal: null
|
||||
})
|
||||
// Layout-owner build: main's load binds the pane again from its tab row while the PTY lives, so
|
||||
// the replacement host publishes it ready at once and never pending.
|
||||
if (!LAYOUT_OWNER_BUILD) {
|
||||
let pendingReceipt: PairedTerminalSnapshotReceipt | null = null
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
pendingReceipt =
|
||||
(await readPairedTerminalSnapshotProbe(client!.page)).receipts.find(
|
||||
(receipt) => receipt.status === 'pending-handle'
|
||||
) ?? null
|
||||
return pendingReceipt
|
||||
},
|
||||
{
|
||||
timeout: 60_000,
|
||||
message: 'Paired renderer never received the replacement host pending-handle frame'
|
||||
}
|
||||
)
|
||||
.not.toBeNull()
|
||||
expect(pendingReceipt).toMatchObject({
|
||||
leafId: terminal.leafId,
|
||||
parentTabId: terminal.parentTabId,
|
||||
ptyId: terminal.ptyId,
|
||||
status: 'pending-handle',
|
||||
terminal: null
|
||||
})
|
||||
}
|
||||
|
||||
let recoveredSurface: HostSurface | null = null
|
||||
await expect
|
||||
@@ -676,6 +683,10 @@ test('retains a verified mirrored PTY binding through a serve restart pending sn
|
||||
ptyId: terminal.ptyId,
|
||||
status: 'ready'
|
||||
})
|
||||
if (LAYOUT_OWNER_BUILD) {
|
||||
const { receipts } = await readPairedTerminalSnapshotProbe(client.page)
|
||||
expect(receipts.filter((receipt) => receipt.status === 'pending-handle')).toEqual([])
|
||||
}
|
||||
|
||||
const recoveredRemotePtyId = toRemoteRuntimePtyId(liveHandle!, client.environmentId)
|
||||
await waitForClientBinding(client, worktreeId, terminal, recoveredRemotePtyId)
|
||||
|
||||
Reference in New Issue
Block a user