mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user