test(terminal): SSH park+reveal e2e round-trip + as-built design notes (C1)

Docker-gated (ORCA_E2E_SSH_DOCKER=1) spec: SSH tab parks behind a decoy and
reveal restores marker content at multi-viewport scrollback depth. DESIGN.md
records the as-built deltas (inline paint, force-park shape, last-active
floor) and the residuals so follow-ups aren't lost.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-07-28 13:21:53 -07:00
co-authored by Orca
parent 59e17342d1
commit 136a34a331
2 changed files with 154 additions and 0 deletions
+56
View File
@@ -333,3 +333,59 @@ targeted vitest suites; no max-lines disables.
- Revert story: each option is a separable commit; B's eviction is additive
to the parking effect and reverts cleanly; A's paint change falls back to
the relay-replay branch which remains intact.
## 8. Implementation notes (as built — deltas and residuals)
Landed as four commits on this branch (A, B, C, plus an A paint-path fix):
1. **A paint is inline, not via `applyMainBufferSnapshot`.** That function
runs its own `structuralReplayCoordinator.run`; calling it from
`applyReattachPayload` (already inside the coordinator when a relay replay
exists) deadlocks on the coordinator's tail chain. The paint mirrors the
daemon-snapshot branch (folded `scrollbackAnsi` + rehydrate + screen,
dimension-matched, escape tail last) and arms
`setRestoredSnapshotBaseline` so deferred/live chunks the snapshot covers
dedupe instead of double-painting.
2. **B is force-park, not unmount-from-`mountedWorktreeIdsRef`.** Force-parked
ids join the existing parked set after the coverage veto, so every
downstream mechanism (render null, watcher sync, reveal) is the ordinary
parking machinery; the only new state is the verdict itself. Blast radius
shrank accordingly.
3. **Last-active exemption applies to eviction too** (ranking reuses
`selectIdsBeyondHotRetain`): a SINGLE hidden un-parkable worktree is never
force-parked — one warm slot is the deliberate floor, matching parking
semantics. The field profile this targets is many-worktree.
4. **Residual: empty relay replay on parked-SSH reveal.** If relay `buffered`
is empty (relay restart), `hasStructuralReplay` is false and nothing
paints (pre-existing app-restart behavior). When the hidden-delivery gate
dropped bytes during the park, main's `pty:modelRestoreNeeded` marker
fires on reveal and repaints from the model with seq dedupe — so the
blank case is confined to relay-restart with no dropped-byte marker.
5. **Residual (pre-existing, unchanged):** parked/evicted fact-mode watchers
omit `onCommandFinished`/`onCommandCode*`; OSC 133;D command-lifecycle
facts drop while parked.
Test coverage vs gate condition 5:
- SSH park+reveal round-trip with scrollback-depth assertion:
`tests/e2e/ssh-terminal-parking.spec.ts` (Docker-gated,
`ORCA_E2E_SSH_DOCKER=1`, same lane as the other SSH relay specs) + unit
coverage of eligibility/coverage/paint-source decisions.
- Folder workspaces: unit parity case (worktree-shaped id equality) in
`terminal-hidden-view-parking.test.ts`; the local parking e2e already runs
on folder-workspace-shaped ids.
- Remote-runtime eviction under B: covered at the selector level
(`terminal-hidden-worktree-retention.test.ts`); a live e2e needs either two
remote worktrees in the rig or a retention-limit e2e override (the
last-active exemption shields a single candidate) — follow-up, noted here
so it isn't lost.
- Fail-open exemption: unit-covered (`isEvictionExemptTerminalTab` + selector
exclusion case).
- C demotion+restore: unit-covered (selector TTL/monotone cases, registry
notify damping, row clamp); demotion applies through the same
`applyTerminalScrollbackRowsToMountedPanes` path the existing
settings-change e2e exercises.
- Flip-loop regression (condition 4): policy-level idempotence +
time-monotone membership tests for both new selectors, plus notify damping
on the demotion registry; the component-level damping conventions (verdict
out of own deps, set-equality bail, positive-delay timers) are preserved
unchanged.
+98
View File
@@ -0,0 +1,98 @@
import type { TestInfo } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady, getActiveTabId } from './helpers/store'
import {
getTerminalContent,
sendToTerminal,
waitForActivePanePtyId,
waitForActiveTerminalManager,
waitForPaneIdentitySnapshot
} from './helpers/terminal'
import { parkHiddenTabBehindDecoy } from './helpers/terminal-hidden-parking'
import {
cleanupDockerSshRelayTarget,
startDockerSshRelayTarget,
type DockerSshRelayTarget
} from './helpers/docker-ssh-relay-target'
import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection'
const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1'
const PARKING_DELAY_MS = Number(process.env.ORCA_E2E_TERMINAL_PARKING_DELAY_MS) || 500
test.use({ seedTestRepo: false })
// C1 slice A: SSH tabs park like local ones and reveal restores content from
// main's headless model (relay replay is the fallback). This is the SSH
// park+reveal round-trip fidelity check the design gate required.
test.describe('SSH terminal hidden view parking', () => {
test.skip(!RUN_DOCKER_SSH, 'Set ORCA_E2E_SSH_DOCKER=1 to run Docker-backed SSH tests.')
test.skip(process.platform === 'win32', 'Docker SSH parking uses POSIX SSH tooling.')
test('parks a hidden SSH tab and restores its scrollback on reveal', async ({
orcaPage
}, testInfo: 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)
const sshPtyId = await waitForActivePanePtyId(orcaPage, 60_000)
const sshTabId = await getActiveTabId(orcaPage)
if (!sshTabId) {
throw new Error('SSH terminal tab did not become active')
}
const snapshot = await waitForPaneIdentitySnapshot(orcaPage, 1)
expect(snapshot.panes[0]?.ptyId).toBe(sshPtyId)
// Why numbered lines: the reveal assertion checks BOTH the final marker
// (screen) and an early line (scrollback depth beyond one viewport).
const marker = `SSH_PARK_MARKER_${Date.now()}`
await sendToTerminal(
orcaPage,
sshPtyId,
`for i in $(seq 1 200); do echo "${marker}_$i"; done\r`
)
await expect
.poll(() => getTerminalContent(orcaPage, 20_000), {
timeout: 30_000,
message: 'SSH marker output did not render before parking'
})
.toContain(`${marker}_200`)
await parkHiddenTabBehindDecoy(orcaPage, remote.worktreeId, sshTabId, {
parkDelayMs: PARKING_DELAY_MS
})
// Reveal: reattach must paint from main's headless model (or relay
// replay when the model is unavailable) — never a blank pane.
await orcaPage.evaluate((tabId) => {
const state = window.__store?.getState()
state?.setActiveTab(tabId)
state?.setActiveTabType('terminal')
}, sshTabId)
await waitForActiveTerminalManager(orcaPage, 60_000)
await expect
.poll(() => getTerminalContent(orcaPage, 20_000), {
timeout: 60_000,
message: 'revealed SSH tab did not restore the final marker line'
})
.toContain(`${marker}_200`)
// Why _150 not _1: the relay-replay fallback is a 100KiB tail, so the
// earliest lines are only guaranteed under the main-model paint; a mid
// marker asserts multi-viewport depth without coupling to either source.
await expect
.poll(() => getTerminalContent(orcaPage, 40_000), {
timeout: 15_000,
message: 'revealed SSH tab lost scrollback beyond the visible screen'
})
.toContain(`${marker}_150`)
} finally {
cleanupDockerSshRelayTarget(target)
}
})
})