mirror of
https://github.com/stablyai/orca.git
synced 2026-09-23 08:02:31 +00:00
The Docker-SSH e2e lane only ran when a PR's changed specs happened to include
`ssh-startup-exec-readiness.spec.ts` or `paired-startup-exec-readiness.spec.ts`.
Editing SSH source itself did not trigger it, and pruning either spec from a
route's list would have silently retired the whole lane. Meanwhile the sharded
lanes set no `ORCA_E2E_SSH_DOCKER`, so every Docker-gated spec skipped itself
while the shard still reported green -- the exact silent-skip shape
`docs/reference/ssh-reconnect-source-recovery.md` blames for four regressions
that reached users.
Separately, the modules that actually own direct-SSH workspace and tab restore
carry no "ssh" in their names, so the `ssh-terminal-source` route never reached
them. Measured on the real script before this change:
printf '%s\n' src/renderer/src/hooks/remote-workspace-session-merge.ts \
src/main/ipc/remote-workspace-snapshot-normalization.ts \
src/renderer/src/lib/worktree-initial-terminal-seeding.ts \
src/shared/remote-workspace-session-projection.ts \
| node config/scripts/pr-e2e-source-routing.mjs
=> []
Three changes, all pinned by the executable gate contract:
- `hasSshSourceChange` derives an `ssh_source_changed` signal from the SSH
routes themselves, plumbed pr.yml -> e2e.yml, so the lane triggers on source
rather than on a spec name surviving in a list. One list, so the two cannot
drift.
- A sibling `ssh-workspace-session-restore` route names the restore seams
(`remote-workspace-*`, `worktree-initial-terminal-seeding`,
`worktree-default-terminal-tabs`, `initial-terminal`) and routes them to the
two restore specs -- a sibling rather than more paths on `ssh-terminal-source`
so a tab-tombstone edit does not run the whole SSH terminal list.
- A new `test:e2e:ssh-docker` runner claims the remaining Docker-gated specs on
the one VM that sets the flag, and the contract now fails by name when any
Docker-gated spec is claimed by no runner. `ssh-docker-relay-perf` and
`ssh-codex-display-artifacts-repro` are recorded exemptions (wall-clock
budgets; needs a real remote codex binary) and the contract asserts each
exemption still corresponds to a real gated spec, so a stale one cannot
quietly excuse a gap. Lane timeout raised 35 -> 60 minutes for the added
serial specs.
The lane's first act was to surface four latent bugs in a spec that had been
silently skipping. `ssh-docker-bulk-open-freeze-repro.spec.ts` is four call sites
out of date against `tests/e2e/helpers/terminal.ts`: `startDockerSshRelayTarget()`
is called with no argument though the helper dereferences `testInfo.workerIndex`
(a 100% failure, not a flake), `execInTerminal` gained a `ptyId` parameter, and
`splitActiveTerminalPane` gained a direction. It was invisible because it ran
nowhere and `typecheck:e2e` is red on main with 240 pre-existing errors, so four
more could not be seen.
The `testInfo` bug is fixed here -- correct on its own, and it removes one real
error from `typecheck:e2e` (240 -> 239). The other three are not, because they
are not argument plumbing: repairing them requires choosing which ptyId to
capture and which split direction to use, and both change what the repro
measures.
The spec is therefore added to the exemption list rather than repaired, for two
independent reasons recorded in the runner: it is a perf oracle, not a
correctness one (`SOFT_FREEZE_LAG_MS=2500` / `HARD_FREEZE_LAG_MS=5000` measured
under a deliberate 5-pane flood on a 420s budget -- the same rule already applied
to `ssh-docker-relay-perf.spec.ts`), and it is known-rotted. Repair is tracked in
stablyai/orca#16764. Applying an existing written rule to a sibling that plainly
meets it is consistency; inventing a new exemption to dodge a red would not be.
Three hardening fixes to the contract itself:
- Runner text is comment-stripped before the claimed-by-a-lane scan. A substring
scan over raw text lets a spec merely *discussed* in a runner comment count as
claimed -- the silent skip this assertion exists to catch, re-entering through
the documentation. Not live today only because the existing comments write the
spec names without their `tests/e2e/` prefix.
- An exempt spec must not be invoked by any runner. `unreachableSpecs`
short-circuits the unclaimed check, so a spec could be documented as exempt
while a runner still ran it -- an exemption that reads as coverage removal but
changes nothing, leaving the lane red for a reason the file says it excluded.
This is not hypothetical: adding the bulk-open exemption without removing it
from the runner's spec list produced exactly that state, and this assertion is
what caught it.
- The Docker-gate detector is now `/ORCA_E2E_SSH_DOCKER\s*[!=]==\s*['"]1['"]/`
rather than one fixed string, so a double-quoted or `!==` spelling can no
longer escape the contract.
`ssh-restart-tab-accumulation.spec.ts` is a new three-cycle restart fence
asserting tab-id set identity, not just the active pane's reclaimed ptyId as
`ssh-cold-activation-restore.spec.ts:241` did. It passes today; it was validated
by a negative control that injected one tab after cycle 1 and correctly failed.
311 lines
13 KiB
TypeScript
311 lines
13 KiB
TypeScript
import type { ElectronApplication } from '@stablyai/playwright-test'
|
|
import { test, expect } from './helpers/orca-app'
|
|
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
|
import {
|
|
focusActiveTerminalInput,
|
|
waitForActivePanePtyId,
|
|
waitForActiveTerminalManager
|
|
} from './helpers/terminal'
|
|
import {
|
|
createRemoteTerminalTab,
|
|
readRemoteTerminalTabs
|
|
} from './helpers/docker-ssh-relay-terminal-tabs'
|
|
import {
|
|
cleanupDockerSshRelayTarget,
|
|
DOCKER_SSH_RELAY_REMOTE_REPO_PATH,
|
|
execDockerSshRelayTargetCommand,
|
|
startDockerSshRelayTarget,
|
|
type DockerSshRelayTarget
|
|
} from './helpers/docker-ssh-relay-target'
|
|
import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection'
|
|
import { createRestartSession } from './helpers/orca-restart'
|
|
|
|
const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1'
|
|
const TAB_COUNT = 6
|
|
|
|
test.use({ seedTestRepo: false })
|
|
|
|
function readRemoteProof(target: DockerSshRelayTarget, path: string): string | null {
|
|
try {
|
|
return execDockerSshRelayTargetCommand(target, `cat ${path}`)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
test.describe('SSH cold activation restore', () => {
|
|
test.skip(!RUN_DOCKER_SSH, 'Set ORCA_E2E_SSH_DOCKER=1 to run Docker-backed SSH tests.')
|
|
test.skip(process.platform === 'win32', 'Docker SSH restore uses POSIX SSH tooling.')
|
|
|
|
test('eagerly remounts every restored remote terminal after renderer reload', async ({
|
|
orcaPage
|
|
}, testInfo) => {
|
|
test.setTimeout(240_000)
|
|
let target: DockerSshRelayTarget | null = null
|
|
try {
|
|
target = startDockerSshRelayTarget(testInfo)
|
|
await waitForSessionReady(orcaPage)
|
|
const remote = await connectDockerSshRelayTarget(orcaPage, target)
|
|
await expect
|
|
.poll(() => waitForActiveWorktree(orcaPage), { timeout: 30_000 })
|
|
.toBe(remote.worktreeId)
|
|
await waitForActiveTerminalManager(orcaPage, 60_000)
|
|
await waitForActivePanePtyId(orcaPage, 60_000)
|
|
|
|
while ((await readRemoteTerminalTabs(orcaPage, remote.worktreeId)).length < TAB_COUNT) {
|
|
await createRemoteTerminalTab(orcaPage, remote.worktreeId)
|
|
}
|
|
const beforeReload = await readRemoteTerminalTabs(orcaPage, remote.worktreeId)
|
|
expect(beforeReload).toHaveLength(TAB_COUNT)
|
|
expect(new Set(beforeReload.map((tab) => tab.ptyId)).size).toBe(TAB_COUNT)
|
|
expect(beforeReload.every((tab) => tab.ptyId !== null)).toBe(true)
|
|
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate(
|
|
async ({ targetId, worktreePath }) => {
|
|
const snapshot = await window.api.remoteWorkspace.get({ targetId })
|
|
return (
|
|
snapshot?.session.tabsByWorktreePath[worktreePath]?.map((tab) => tab.id) ?? []
|
|
)
|
|
},
|
|
{
|
|
targetId: remote.targetId,
|
|
worktreePath: DOCKER_SSH_RELAY_REMOTE_REPO_PATH
|
|
}
|
|
),
|
|
{ timeout: 30_000, message: 'SSH tabs were not committed to the relay workspace' }
|
|
)
|
|
.toEqual(beforeReload.map((tab) => tab.id))
|
|
|
|
await orcaPage.evaluate(() => window.dispatchEvent(new Event('beforeunload')))
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate(
|
|
async ({ targetId, worktreeId, expectedTabIds }) => {
|
|
const session = await window.api.session.get()
|
|
const persistedTabIds = new Set(
|
|
(session.tabsByWorktree[worktreeId] ?? []).map((tab) => tab.id)
|
|
)
|
|
return (
|
|
session.activeConnectionIdsAtShutdown?.includes(targetId) === true &&
|
|
expectedTabIds.every((tabId) => persistedTabIds.has(tabId))
|
|
)
|
|
},
|
|
{
|
|
targetId: remote.targetId,
|
|
worktreeId: remote.worktreeId,
|
|
expectedTabIds: beforeReload.map((tab) => tab.id)
|
|
}
|
|
),
|
|
{ timeout: 10_000, message: 'SSH tabs and active target were not persisted' }
|
|
)
|
|
.toBe(true)
|
|
|
|
await orcaPage.reload()
|
|
await waitForSessionReady(orcaPage, 60_000)
|
|
await expect
|
|
.poll(() => waitForActiveWorktree(orcaPage), { timeout: 60_000 })
|
|
.toBe(remote.worktreeId)
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate(
|
|
(targetId) => window.__store?.getState().sshConnectionStates.get(targetId)?.status,
|
|
remote.targetId
|
|
),
|
|
{ timeout: 60_000, message: 'renderer SSH state did not restore' }
|
|
)
|
|
.toBe('connected')
|
|
|
|
const expectedTabIds = beforeReload.map((tab) => tab.id).sort()
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
orcaPage.evaluate(
|
|
(ids) => ids.filter((tabId) => window.__paneManagers?.has(tabId)).sort(),
|
|
expectedTabIds
|
|
),
|
|
{ timeout: 60_000, message: 'not every restored SSH tab mounted a PaneManager' }
|
|
)
|
|
.toEqual(expectedTabIds)
|
|
expect(
|
|
await orcaPage.evaluate(
|
|
(ids) =>
|
|
ids.filter((tabId) => window.__terminalParkingDebug?.parkedTabIds().includes(tabId)),
|
|
expectedTabIds
|
|
)
|
|
).toEqual([])
|
|
const afterReload = await readRemoteTerminalTabs(orcaPage, remote.worktreeId)
|
|
expect(afterReload.map((tab) => tab.id).sort()).toEqual(expectedTabIds)
|
|
expect(afterReload.map((tab) => tab.ptyId).sort()).toEqual(
|
|
beforeReload.map((tab) => tab.ptyId).sort()
|
|
)
|
|
|
|
const firstTabId = beforeReload[0]?.id
|
|
if (!firstTabId) {
|
|
throw new Error('Restored SSH tabs disappeared')
|
|
}
|
|
// Six restored tabs overflow the strip at CI's window size and the restore pins it to the END,
|
|
// so Terminal 1 starts outside the scroll viewport. Neither `click()` nor
|
|
// `scrollIntoViewIfNeeded()` can reach it: both wait for the element to hold still, and the
|
|
// strip keeps re-laying-out while the relay reconnects behind it — so they time out on an
|
|
// element they can see but never settle on. This spec had never run in CI before this branch
|
|
// routed it there, which is why that only shows up now.
|
|
//
|
|
// So the pointer is driven directly, and the whole attempt retried, which needs no element to
|
|
// be stable — only to be somewhere at the moment it is pressed. Activation is deferred to
|
|
// pointerup and suppressed past a drag threshold (tab-strip-pointer-activation.ts), so this
|
|
// has to be a real down/up pair at one position; a synthetic click event would not select.
|
|
// The retry asserts on the store, so a press that lands wrong is retried rather than believed.
|
|
const tabStrip = orcaPage.locator('.terminal-tab-strip').first()
|
|
const firstTab = orcaPage.getByRole('button', { name: /^Terminal 1 Close tab Terminal 1/ })
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
await tabStrip.evaluate((el) => {
|
|
el.scrollLeft = 0
|
|
})
|
|
const box = await firstTab.boundingBox()
|
|
if (!box) {
|
|
return null
|
|
}
|
|
await orcaPage.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
|
await orcaPage.mouse.down()
|
|
await orcaPage.mouse.up()
|
|
return orcaPage.evaluate(() => window.__store?.getState().activeTabId ?? null)
|
|
},
|
|
{
|
|
timeout: 30_000,
|
|
message: 'pressing the restored first tab never made it active'
|
|
}
|
|
)
|
|
.toBe(firstTabId)
|
|
await orcaPage.evaluate((tabId) => {
|
|
const manager = window.__paneManagers?.get(tabId)
|
|
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0]
|
|
if (!pane) {
|
|
throw new Error('Restored SSH pane unavailable')
|
|
}
|
|
pane.terminal.options.screenReaderMode = true
|
|
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
|
}, firstTabId)
|
|
|
|
const marker = `SSH_RESTORE_OK_${Date.now()}`
|
|
const proofFile = '/tmp/orca-ssh-restore-proof'
|
|
await focusActiveTerminalInput(orcaPage)
|
|
await orcaPage.keyboard.type(`printf '${marker}' > ${proofFile} && printf '${marker}\\n'`)
|
|
await orcaPage.keyboard.press('Enter')
|
|
await expect(
|
|
orcaPage.locator(
|
|
`[data-terminal-tab-id=${JSON.stringify(firstTabId)}] .xterm-accessibility-tree`
|
|
)
|
|
).toContainText(marker, { timeout: 30_000 })
|
|
expect(execDockerSshRelayTargetCommand(target, `cat ${proofFile}`)).toBe(marker)
|
|
} finally {
|
|
cleanupDockerSshRelayTarget(target)
|
|
}
|
|
})
|
|
|
|
test('reclaims the authenticated PTY owner immediately after a full app restart', async (// oxlint-disable-next-line no-empty-pattern -- This restart test owns both Electron launches.
|
|
{}, testInfo) => {
|
|
test.setTimeout(300_000)
|
|
const restart = createRestartSession(testInfo)
|
|
let target: DockerSshRelayTarget | null = null
|
|
let firstApp: ElectronApplication | null = null
|
|
let secondApp: ElectronApplication | null = null
|
|
try {
|
|
target = startDockerSshRelayTarget(testInfo)
|
|
const firstLaunch = await restart.launch()
|
|
firstApp = firstLaunch.app
|
|
await waitForSessionReady(firstLaunch.page)
|
|
const remote = await connectDockerSshRelayTarget(firstLaunch.page, target)
|
|
await expect
|
|
.poll(() => waitForActiveWorktree(firstLaunch.page), { timeout: 30_000 })
|
|
.toBe(remote.worktreeId)
|
|
await waitForActiveTerminalManager(firstLaunch.page, 60_000)
|
|
const firstPtyId = await waitForActivePanePtyId(firstLaunch.page, 60_000)
|
|
const token = `SSH_PROCESS_RESTART_${Date.now()}`
|
|
const beforeProofPath = `/tmp/orca-ssh-restart-before-${Date.now()}`
|
|
const afterProofPath = `/tmp/orca-ssh-restart-after-${Date.now()}`
|
|
|
|
await focusActiveTerminalInput(firstLaunch.page)
|
|
await firstLaunch.page.keyboard.type(
|
|
`export ORCA_RESTART_TOKEN=${token}; cd /tmp; (while :; do sleep 60; done) & export ORCA_BG_PID=$!; printf '%s|%s|%s|%s\\n' "$$" "$ORCA_BG_PID" "$ORCA_RESTART_TOKEN" "$PWD" > ${beforeProofPath}`
|
|
)
|
|
await firstLaunch.page.keyboard.press('Enter')
|
|
await expect.poll(() => readRemoteProof(target!, beforeProofPath)).not.toBeNull()
|
|
const beforeProof = readRemoteProof(target, beforeProofPath)
|
|
expect(beforeProof).toMatch(/^\d+\|\d+\|SSH_PROCESS_RESTART_\d+\|\/tmp$/)
|
|
|
|
const beforeTabs = await readRemoteTerminalTabs(firstLaunch.page, remote.worktreeId)
|
|
const restoredTabId = beforeTabs.find((tab) => tab.ptyId === firstPtyId)?.id
|
|
if (!restoredTabId) {
|
|
throw new Error('Active SSH terminal was not persisted in its worktree')
|
|
}
|
|
await firstLaunch.page.evaluate(() => window.dispatchEvent(new Event('beforeunload')))
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
firstLaunch.page.evaluate(
|
|
async ({ targetId, worktreeId, tabId }) => {
|
|
const persisted = await window.api.session.get()
|
|
return (
|
|
persisted.activeConnectionIdsAtShutdown?.includes(targetId) === true &&
|
|
persisted.tabsByWorktree[worktreeId]?.some((tab) => tab.id === tabId) === true
|
|
)
|
|
},
|
|
{ targetId: remote.targetId, worktreeId: remote.worktreeId, tabId: restoredTabId }
|
|
),
|
|
{ timeout: 10_000, message: 'SSH restart state was not persisted before quit' }
|
|
)
|
|
.toBe(true)
|
|
|
|
await restart.close(firstApp)
|
|
firstApp = null
|
|
|
|
const secondLaunch = await restart.launch()
|
|
secondApp = secondLaunch.app
|
|
await waitForSessionReady(secondLaunch.page, 60_000)
|
|
await expect
|
|
.poll(() => waitForActiveWorktree(secondLaunch.page), { timeout: 60_000 })
|
|
.toBe(remote.worktreeId)
|
|
await waitForActiveTerminalManager(secondLaunch.page, 60_000)
|
|
expect(await waitForActivePanePtyId(secondLaunch.page, 60_000)).toBe(firstPtyId)
|
|
await secondLaunch.page.evaluate((tabId) => {
|
|
const manager = window.__paneManagers?.get(tabId)
|
|
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0]
|
|
if (!pane) {
|
|
throw new Error('Restored SSH pane unavailable')
|
|
}
|
|
pane.terminal.options.screenReaderMode = true
|
|
pane.terminal.refresh(0, pane.terminal.rows - 1)
|
|
}, restoredTabId)
|
|
|
|
const restoredMarker = `SSH_OWNER_RESTORED_${Date.now()}`
|
|
await focusActiveTerminalInput(secondLaunch.page)
|
|
await secondLaunch.page.keyboard.type(
|
|
`printf '%s|%s|%s|%s\\n' "$$" "$ORCA_BG_PID" "$ORCA_RESTART_TOKEN" "$PWD" > ${afterProofPath}; printf '${restoredMarker}\\n'`
|
|
)
|
|
await secondLaunch.page.keyboard.press('Enter')
|
|
await expect(
|
|
secondLaunch.page.locator(
|
|
`[data-terminal-tab-id=${JSON.stringify(restoredTabId)}] .xterm-accessibility-tree`
|
|
)
|
|
).toContainText(restoredMarker, { timeout: 30_000 })
|
|
await expect.poll(() => readRemoteProof(target!, afterProofPath)).toBe(beforeProof)
|
|
} finally {
|
|
if (secondApp) {
|
|
await restart.close(secondApp)
|
|
}
|
|
if (firstApp) {
|
|
await restart.close(firstApp)
|
|
}
|
|
await restart.dispose()
|
|
cleanupDockerSshRelayTarget(target)
|
|
}
|
|
})
|
|
})
|