mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
test(ssh): cover paired split lifecycle end to end
This commit is contained in:
@@ -6624,7 +6624,7 @@
|
||||
"providers": ["ssh", "remote-runtime", "wsl"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["ssh", "remote-runtime"],
|
||||
"coverageNotes": "Deterministic main/controller coverage proves renderer-owned and paired/headless SSH spawns durably precommit cleanup-only ownership before binding publication, atomically promote accepted panes, and retain retryable cleanup authority without restoring a rejected pane when shutdown fails. Deterministic renderer coverage proves startup publishes the state returned by ssh.connect, retained native and runtime SSH payloads are admitted through production routes only with valid complete authority, stale cleanup cannot unregister a replacement runtime terminal, direct SSH Git and folder panes clear and retry by exact authority, one authority chain stops after two automatic attempts even when each timeout exceeds the rolling window, rejected acknowledgements mutate no store maps, and one shared exact attempt admits every concurrent split-pane spawn and reattach while preserving the first PTY as the tab fallback. A later sibling failure rotates the tab once, stale callbacks from the prior attempt mutate no state, split remount activity suppression is counted per leaf, primary PTY exit promotes a bound survivor or preserves an empty continuation gap for a late sibling, and primary, non-primary, or null-PTY detach preserves exact authority on both resulting tabs. Intentional pane disposal cancels its settlement timer without breaking StrictMode remount timeout ownership. Target snapshot hydration/reconnect preserves sibling SSH/local/WSL/runtime state, and a mounted remote-runtime terminal survives repeated transport partitions without changing PTY identity. A real encrypted-WebSocket oracle proves a successful reachability probe can replace a pre-ready shared-control socket without rejecting or duplicating the waiting RPC. Direct SSH coordinator tests cover immediate terminal finalization, hydration correction, damping, bounded retry, and telemetry non-interference. Client/server heartbeat tests cover timer suspension, socket generations fence stale callbacks, cold restored-terminal attachment retries, cached pixels remain unhealthy until authoritative replay, automatic retries stop after one minute, manual reconnect preserves the PTY, and pane closure releases recovery UI state. Current macOS Electron journeys against an ephemeral Linux Docker SSH target cover exact-authority repo/worktree hydration, live terminal recovery after disconnect/reconnect, and eager six-terminal remount after renderer reload. A Windows remote-runtime smoke covers reachability and PTY round-trip. Multi-target live fanout, paired-close, WSL, and patched live partition journeys remain gaps.",
|
||||
"coverageNotes": "Deterministic main/controller coverage proves renderer-owned and paired/headless SSH spawns durably precommit cleanup-only ownership before binding publication, atomically promote accepted panes, and retain retryable cleanup authority without restoring a rejected pane when shutdown fails. Deterministic renderer coverage proves startup publishes the state returned by ssh.connect, retained native and runtime SSH payloads are admitted through production routes only with valid complete authority, stale cleanup cannot unregister a replacement runtime terminal, direct SSH Git and folder panes clear and retry by exact authority, one authority chain stops after two automatic attempts even when each timeout exceeds the rolling window, rejected acknowledgements mutate no store maps, and one shared exact attempt admits every concurrent split-pane spawn and reattach while preserving the first PTY as the tab fallback. A later sibling failure rotates the tab once, stale callbacks from the prior attempt mutate no state, split remount activity suppression is counted per leaf, primary PTY exit promotes a bound survivor or preserves an empty continuation gap for a late sibling, and primary, non-primary, or null-PTY detach preserves exact authority on both resulting tabs. Intentional pane disposal cancels its settlement timer without breaking StrictMode remount timeout ownership. Target snapshot hydration/reconnect preserves sibling SSH/local/WSL/runtime state, and a mounted remote-runtime terminal survives repeated transport partitions without changing PTY identity. A real encrypted-WebSocket oracle proves a successful reachability probe can replace a pre-ready shared-control socket without rejecting or duplicating the waiting RPC. Direct SSH coordinator tests cover immediate terminal finalization, hydration correction, damping, bounded retry, and telemetry non-interference. Client/server heartbeat tests cover timer suspension, socket generations fence stale callbacks, cold restored-terminal attachment retries, cached pixels remain unhealthy until authoritative replay, automatic retries stop after one minute, manual reconnect preserves the PTY, and pane closure releases recovery UI state. Current macOS Electron journeys against an ephemeral Linux Docker SSH target cover exact-authority repo/worktree hydration, live terminal recovery after disconnect/reconnect, eager six-terminal remount after renderer reload, and supplemental paired headed/hidden-desktop split ownership with exact UI-to-runtime-handle mapping and independent remote-shell lifecycle assertions. Renderer-free `orca serve` does not register direct-SSH targets, so shared headless spawn behavior is covered deterministically rather than claimed as a live nested-SSH serve journey. A Windows remote-runtime smoke covers reachability and PTY round-trip. Multi-target live fanout, paired-close, WSL, and patched live partition journeys remain gaps.",
|
||||
"motivatingLinks": [
|
||||
"https://github.com/stablyai/orca/pull/6951",
|
||||
"https://github.com/stablyai/orca/pull/6955",
|
||||
@@ -6646,6 +6646,8 @@
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/repos-remote.test.ts src/main/ipc/ssh.test.ts src/main/ipc/worktrees.test.ts src/main/runtime/public-ssh-state.test.ts src/main/ssh/ssh-connection-manager.test.ts src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-provider-authority.test.ts src/preload/ssh-authority-forwarding.test.ts src/renderer/src/runtime/runtime-client-events.test.ts src/renderer/src/runtime/runtime-environment-ssh-state.test.ts src/shared/ssh-retained-payload-admission.test.ts src/shared/ssh-types.test.ts --reporter=dot",
|
||||
"pnpm exec electron-vite build --mode e2e",
|
||||
"pnpm run build:web-from-renderer",
|
||||
"ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-split-lifecycle.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
|
||||
"ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-split-lifecycle.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/terminal-cold-activation-deferral.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-cold-activation-restore.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
@@ -6702,10 +6704,17 @@
|
||||
"src/shared/ssh-retained-payload-admission.test.ts",
|
||||
"src/shared/ssh-types.test.ts",
|
||||
"tests/e2e/terminal-cold-activation-deferral.spec.ts",
|
||||
"tests/e2e/ssh-split-lifecycle.spec.ts",
|
||||
"tests/e2e/ssh-cold-activation-restore.spec.ts",
|
||||
"tests/e2e/ssh-docker-relay-perf.spec.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
{
|
||||
"file": "tests/e2e/ssh-split-lifecycle.spec.ts",
|
||||
"assertions": [
|
||||
"supplemental paired headed and hidden-desktop journeys bind each rendered pane to its exact runtime handle, create two distinct Linux TTY shells, stream through the rendered split, retire only the selected pane and shell, preserve the source plus fresh-terminal creation, and make no client-local SSH connection"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/ipc/pty.test.ts",
|
||||
"assertions": [
|
||||
@@ -7019,14 +7028,32 @@
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-split-lifecycle.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
|
||||
"result": "passed",
|
||||
"durationSeconds": 28.1,
|
||||
"summary": "A paired headed Electron client split a runtime-owned SSH terminal against an ephemeral Linux sshd, mapped both rendered panes to exact runtime handles, observed two distinct remote TTY shells, streamed through the rendered split, killed only the selected split shell, kept the source plus fresh-terminal creation live, and made no local SSH connection."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-13",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-split-lifecycle.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
|
||||
"result": "passed",
|
||||
"durationSeconds": 28.2,
|
||||
"summary": "The same supplemental paired SSH split, exact rendered binding, stream, selective close, source-survival, fresh-terminal, and no-local-fallback oracle passed with a hidden desktop host; this is not renderer-free orca serve coverage."
|
||||
},
|
||||
{
|
||||
"date": "2026-08-12",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty.test.ts src/main/persistence.test.ts src/main/ssh/ssh-relay-session.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 23.38,
|
||||
"summary": "Three main/controller files and 982 tests passed, including cleanup-only precommit, atomic binding promotion, failed-shutdown retry ownership, rejected-pane non-restoration, lease identity replacement, and reconnect reclamation."
|
||||
"durationSeconds": 18.02,
|
||||
"summary": "Three main/controller files and 978 tests passed, including cleanup-only precommit, atomic binding promotion, failed-shutdown retry ownership, rejected-pane non-restoration, lease identity replacement, and reconnect reclamation."
|
||||
},
|
||||
{
|
||||
"date": "2026-07-14",
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import type { Page, TestInfo } from '@stablyai/playwright-test'
|
||||
|
||||
// Supplemental transport/UI coverage; the rejected-cleanup red/green oracle lives in pty.test.ts.
|
||||
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import {
|
||||
cleanupDockerSshRelayTarget,
|
||||
execDockerSshRelayTargetCommand,
|
||||
startDockerSshRelayTarget,
|
||||
type DockerSshRelayTarget
|
||||
} from './helpers/docker-ssh-relay-target'
|
||||
import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection'
|
||||
import {
|
||||
createRuntimeDesktopPairingOffer,
|
||||
launchPairedElectronClient,
|
||||
type PairedElectronClient
|
||||
} from './helpers/paired-electron-client'
|
||||
import {
|
||||
assertInteractiveTerminal,
|
||||
assertPairedTerminalCreation
|
||||
} from './helpers/nested-runtime-ssh-client-route'
|
||||
import { countVisibleTerminalPanes } from './helpers/terminal'
|
||||
|
||||
const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1'
|
||||
|
||||
function readRemoteTtyShellPids(target: DockerSshRelayTarget): string[] {
|
||||
const output = execDockerSshRelayTargetCommand(
|
||||
target,
|
||||
'ps -eo pid=,tty=,comm= | awk \'$2 != "?" && $3 ~ /^(bash|sh|zsh)$/ { print $1 }\''
|
||||
)
|
||||
return output ? output.split(/\s+/).sort((a, b) => Number(a) - Number(b)) : []
|
||||
}
|
||||
|
||||
function remoteTerminalHandle(ptyId: string): string {
|
||||
const separator = ptyId.indexOf('@@')
|
||||
if (!ptyId.startsWith('remote:') || separator === -1) {
|
||||
throw new Error(`Expected a runtime-owned PTY id, received ${ptyId}`)
|
||||
}
|
||||
return decodeURIComponent(ptyId.slice(separator + 2))
|
||||
}
|
||||
|
||||
async function visiblePanePtyIds(page: Page): Promise<string[]> {
|
||||
return page.locator('[data-pty-id]').evaluateAll((elements) =>
|
||||
elements.flatMap((element) => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const style = window.getComputedStyle(element)
|
||||
const ptyId = (element as HTMLElement).dataset.ptyId
|
||||
return ptyId && rect.width > 0 && style.display !== 'none' && style.visibility !== 'hidden'
|
||||
? [ptyId]
|
||||
: []
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function terminalMarkerCommand(marker: string): string {
|
||||
const encoded = [...marker]
|
||||
.map((character) => `\\${character.charCodeAt(0).toString(8).padStart(3, '0')}`)
|
||||
.join('')
|
||||
return `printf '${encoded}\\n'`
|
||||
}
|
||||
|
||||
async function activateHostLocalWorkspace(page: Page, remoteWorktreeId: string): Promise<void> {
|
||||
const localWorktreeId = await page.evaluate((remoteWorktreeId) => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('Host store is unavailable')
|
||||
}
|
||||
const state = store.getState()
|
||||
const localRepoIds = new Set(
|
||||
state.repos.filter((repo) => !repo.connectionId).map((repo) => repo.id)
|
||||
)
|
||||
const localWorktree = Object.entries(state.worktreesByRepo)
|
||||
.filter(([repoId]) => localRepoIds.has(repoId))
|
||||
.flatMap(([, worktrees]) => worktrees)
|
||||
.find((worktree) => worktree.id !== remoteWorktreeId)
|
||||
if (!localWorktree) {
|
||||
throw new Error('Host has no isolated local workspace for the paired topology')
|
||||
}
|
||||
state.setActiveWorktree(localWorktree.id)
|
||||
return localWorktree.id
|
||||
}, remoteWorktreeId)
|
||||
await expect
|
||||
.poll(() =>
|
||||
page
|
||||
.locator('[data-rendered-active-worktree-id]')
|
||||
.getAttribute('data-rendered-active-worktree-id')
|
||||
)
|
||||
.toBe(localWorktreeId)
|
||||
}
|
||||
|
||||
async function replaceWithRuntimeOwnedSource(
|
||||
client: PairedElectronClient,
|
||||
worktreeId: string
|
||||
): Promise<string> {
|
||||
return client.page.evaluate(
|
||||
async ({ environmentId, worktreeId }) => {
|
||||
const selector = `id:${worktreeId}`
|
||||
const listed = await window.api.runtimeEnvironments.call({
|
||||
selector: environmentId,
|
||||
method: 'terminal.list',
|
||||
params: { worktree: selector }
|
||||
})
|
||||
if (!listed.ok) {
|
||||
throw new Error(`Could not list the SSH source terminals: ${JSON.stringify(listed)}`)
|
||||
}
|
||||
const terminals = (listed.result as { terminals?: { handle: string }[] }).terminals ?? []
|
||||
for (const terminal of terminals) {
|
||||
const closed = await window.api.runtimeEnvironments.call({
|
||||
selector: environmentId,
|
||||
method: 'terminal.closeTab',
|
||||
params: { terminal: terminal.handle }
|
||||
})
|
||||
if (!closed.ok) {
|
||||
throw new Error(`Could not close the initial SSH source: ${JSON.stringify(closed)}`)
|
||||
}
|
||||
}
|
||||
const created = await window.api.runtimeEnvironments.call({
|
||||
selector: environmentId,
|
||||
method: 'terminal.create',
|
||||
params: { worktree: selector, title: 'STA-4023 SSH split source' }
|
||||
})
|
||||
if (!created.ok) {
|
||||
throw new Error(`Could not create the SSH split source: ${JSON.stringify(created)}`)
|
||||
}
|
||||
return (created.result as { terminal: { handle: string } }).terminal.handle
|
||||
},
|
||||
{ environmentId: client.environmentId, worktreeId }
|
||||
)
|
||||
}
|
||||
|
||||
async function readRenderedPane(client: PairedElectronClient, ptyId: string): Promise<string> {
|
||||
return client.page.evaluate((ptyId) => {
|
||||
for (const manager of window.__paneManagers?.values() ?? []) {
|
||||
const pane = manager
|
||||
.getPanes?.()
|
||||
.find((candidate) => candidate.container.dataset.ptyId === ptyId)
|
||||
if (pane) {
|
||||
return pane.serializeAddon?.serialize?.() ?? ''
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}, ptyId)
|
||||
}
|
||||
|
||||
async function sendRenderedMarker(
|
||||
client: PairedElectronClient,
|
||||
ptyId: string,
|
||||
marker: string
|
||||
): Promise<void> {
|
||||
const terminal = client.page.locator(`[data-pty-id=${JSON.stringify(ptyId)}]`)
|
||||
await terminal.locator('.xterm-helper-textarea').focus()
|
||||
await client.page.keyboard.insertText(terminalMarkerCommand(marker))
|
||||
await client.page.keyboard.press('Enter')
|
||||
await expect.poll(() => readRenderedPane(client, ptyId), { timeout: 30_000 }).toContain(marker)
|
||||
}
|
||||
|
||||
async function runPairedSshSplitLifecycle(
|
||||
client: PairedElectronClient,
|
||||
target: DockerSshRelayTarget,
|
||||
repoId: string,
|
||||
worktreeId: string
|
||||
): Promise<void> {
|
||||
const sourceHandle = await replaceWithRuntimeOwnedSource(client, worktreeId)
|
||||
const source = await assertInteractiveTerminal(client, repoId, `STA4023_SOURCE_${Date.now()}`)
|
||||
expect(remoteTerminalHandle(source.ptyId)).toBe(sourceHandle)
|
||||
|
||||
const panesBefore = await countVisibleTerminalPanes(client.page)
|
||||
const shellPidsBefore = readRemoteTtyShellPids(target)
|
||||
expect(shellPidsBefore).toHaveLength(1)
|
||||
expect(await visiblePanePtyIds(client.page)).toContain(source.ptyId)
|
||||
|
||||
const response = await client.page.evaluate(
|
||||
({ environmentId, terminal }) =>
|
||||
window.api.runtimeEnvironments.call({
|
||||
selector: environmentId,
|
||||
method: 'terminal.split',
|
||||
params: { terminal, direction: 'vertical' }
|
||||
}),
|
||||
{ environmentId: client.environmentId, terminal: sourceHandle }
|
||||
)
|
||||
expect(response.ok).toBe(true)
|
||||
const splitHandle = (response.result as { split: { handle: string } }).split.handle
|
||||
expect(splitHandle).not.toBe(sourceHandle)
|
||||
|
||||
await expect.poll(() => countVisibleTerminalPanes(client!.page)).toBe(panesBefore + 1)
|
||||
let splitPtyId = ''
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const ptyIds = await visiblePanePtyIds(client.page)
|
||||
splitPtyId =
|
||||
ptyIds.find((ptyId) => {
|
||||
try {
|
||||
return remoteTerminalHandle(ptyId) === splitHandle
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}) ?? ''
|
||||
return { source: ptyIds.includes(source.ptyId), split: splitPtyId !== '' }
|
||||
})
|
||||
.toEqual({ source: true, split: true })
|
||||
let splitShellPid = ''
|
||||
await expect
|
||||
.poll(() => {
|
||||
const shellPids = readRemoteTtyShellPids(target!)
|
||||
splitShellPid = shellPids.find((pid) => !shellPidsBefore.includes(pid)) ?? ''
|
||||
return shellPids.length
|
||||
})
|
||||
.toBe(2)
|
||||
expect(splitPtyId).not.toBe(source.ptyId)
|
||||
expect(splitShellPid).not.toBe('')
|
||||
|
||||
const splitMarker = `STA4023_SPLIT_${Date.now()}`
|
||||
await sendRenderedMarker(client, splitPtyId, splitMarker)
|
||||
|
||||
const closed = await client.page.evaluate(
|
||||
({ environmentId, terminal }) =>
|
||||
window.api.runtimeEnvironments.call({
|
||||
selector: environmentId,
|
||||
method: 'terminal.close',
|
||||
params: { terminal }
|
||||
}),
|
||||
{ environmentId: client.environmentId, terminal: splitHandle }
|
||||
)
|
||||
expect(closed.ok).toBe(true)
|
||||
await expect.poll(() => readRemoteTtyShellPids(target)).not.toContain(splitShellPid)
|
||||
await expect.poll(() => countVisibleTerminalPanes(client!.page)).toBe(panesBefore)
|
||||
await expect.poll(() => visiblePanePtyIds(client.page)).not.toContain(splitPtyId)
|
||||
expect(readRemoteTtyShellPids(target)).toContain(shellPidsBefore[0])
|
||||
|
||||
await sendRenderedMarker(client, source.ptyId, `STA4023_SOURCE_SURVIVES_${Date.now()}`)
|
||||
await assertPairedTerminalCreation(client, `STA4023_FRESH_TERMINAL_${Date.now()}`)
|
||||
expect(await client.getDirectSshAttemptTargetIds()).toEqual([])
|
||||
}
|
||||
|
||||
async function runHeadedSshSplitLifecycle(hubPage: Page, testInfo: TestInfo): Promise<void> {
|
||||
test.setTimeout(360_000)
|
||||
let target: DockerSshRelayTarget | null = null
|
||||
let client: PairedElectronClient | null = null
|
||||
try {
|
||||
target = startDockerSshRelayTarget(testInfo)
|
||||
const remote = await connectDockerSshRelayTarget(hubPage, target)
|
||||
await activateHostLocalWorkspace(hubPage, remote.worktreeId)
|
||||
const offer = await createRuntimeDesktopPairingOffer(hubPage)
|
||||
client = await launchPairedElectronClient(offer, testInfo, 'STA-4023 SSH split lifecycle')
|
||||
await runPairedSshSplitLifecycle(client, target, remote.repoId, remote.worktreeId)
|
||||
} finally {
|
||||
try {
|
||||
await client?.dispose()
|
||||
} finally {
|
||||
cleanupDockerSshRelayTarget(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('paired SSH split lifecycle', () => {
|
||||
test.skip(!RUN_DOCKER_SSH, 'Set ORCA_E2E_SSH_DOCKER=1 to run the Docker SSH oracle.')
|
||||
test.skip(process.platform === 'win32', 'The disposable SSH target uses POSIX tooling.')
|
||||
|
||||
test('headed runtime owns both SSH split panes and closes only the selected pane @headful', async ({
|
||||
orcaPage
|
||||
}, testInfo) => runHeadedSshSplitLifecycle(orcaPage, testInfo))
|
||||
|
||||
test('hidden desktop host preserves SSH split and close parity', async ({ orcaPage }, testInfo) =>
|
||||
runHeadedSshSplitLifecycle(orcaPage, testInfo))
|
||||
})
|
||||
Reference in New Issue
Block a user