diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a94a7ea2ba5..39e59fab3a6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -267,6 +267,7 @@ jobs: if: >- inputs.test_files == '' || inputs.ssh_source_changed == 'true' || + contains(inputs.test_files, 'tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-startup-exec-readiness.spec.ts') || contains(inputs.test_files, 'tests/e2e/paired-startup-exec-readiness.spec.ts') runs-on: ubuntu-latest diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index d48a239354f..d12539a791e 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -18046,10 +18046,12 @@ "invariant": "Transport loss and frozen-host silence must preserve the remote session; host relay loss may rebind a pane without accumulating reattachable leases. Reconnects must preserve usable terminal content, bounded PTYs/fds/processes, complete large listings, and independently recoverable watcher processes. Electron test shutdown must release inherited pipes after confirmed root exit without closing live-process pipes.", "oracle": "Poll a changed connected SSH authority after injected faults, then require terminal output and appropriate PTY identity. Read remote process/fd state, listFiles replies, and rendered explorer rows. Resolve Playwright cleanup only after the root process exits and its inherited pipes close; live-process pipes remain untouched.", "commands": [ + "ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-docker-five-pane-input-under-flood.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-docker-transport-drop-recovery.spec.ts tests/e2e/ssh-docker-half-open-link.spec.ts tests/e2e/ssh-docker-quick-open-large-listing.spec.ts tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts tests/e2e/ssh-docker-resource-accumulation.spec.ts tests/e2e/ssh-docker-watcher-isolation.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/helpers/electron-process-shutdown.unit.test.ts" ], "testFiles": [ + "tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts", "tests/e2e/ssh-docker-transport-drop-recovery.spec.ts", "tests/e2e/ssh-docker-half-open-link.spec.ts", "tests/e2e/ssh-docker-quick-open-large-listing.spec.ts", @@ -18059,6 +18061,12 @@ "tests/e2e/helpers/electron-process-shutdown.unit.test.ts" ], "assertionRefs": [ + { + "file": "tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts", + "assertions": [ + "five distinct SSH PTYs acknowledge actual keyboard input after two rendered hide/reopen cycles while all five producers flood" + ] + }, { "file": "tests/e2e/ssh-docker-transport-drop-recovery.spec.ts", "assertions": [ @@ -18145,6 +18153,7 @@ "Collect CI runtime and flake history plus product red/green evidence before blocking." ], "knownGaps": [ + "Five-pane simultaneous flood input is newly registered and awaiting CI evidence; it does not resolve the separate #16764 wall-clock performance oracle.", "The disconnected 48MB flood still loses its relay channel: original post-flood input marker failed in 60s, and waiting for the finite producer completion marker failed in 120s. It remains an explicit #18018 fixme reproduction; frozen-host input is re-enabled after four successful runs.", "Linux and Windows desktop clients, WSL, folder workspaces, paired runtimes and live agent CLIs are not exercised by these Docker specs.", "Some legacy assertions inspect terminal serialization or backing state rather than rendered DOM; no blanket visual coverage claim.", diff --git a/config/scripts/run-ssh-docker-e2e.mjs b/config/scripts/run-ssh-docker-e2e.mjs index 9ab44b8457e..d3d8e1519f8 100644 --- a/config/scripts/run-ssh-docker-e2e.mjs +++ b/config/scripts/run-ssh-docker-e2e.mjs @@ -54,10 +54,9 @@ if (runtime.status !== 0) { // the product. Same rule as ssh-docker-relay-perf above. Gating needs a distribution // first, then a host-relative oracle; a bigger constant, or a ratio picked from three // samples, is the same arbitrary number in different clothes. -// COVERAGE GAP, recorded as such: 5 simultaneously flooding SSH panes exercise writer -// saturation, ACK/credit accounting and per-pane polling together, and nothing else covers -// that combination. Flip `test.fixme` back to `test` to run it. Tracked in -// stablyai/orca#16764. +// The five-pane-input-under-flood spec below checks functional keyboard delivery during +// simultaneous output and hide/reopen cycles; its initial CI validation is pending. +// It does not replace this timing oracle. Performance coverage remains a gap in #16764. // // Why both projects: ssh-port-forward-lifecycle is @headful, which the headless project // grep-inverts away. @@ -87,6 +86,7 @@ const result = spawnSync( 'tests/e2e/ssh-ai-vault-session-history.spec.ts', 'tests/e2e/ssh-cold-activation-restore.spec.ts', 'tests/e2e/ssh-cold-hydration-gap-tab-seeding.spec.ts', + 'tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts', 'tests/e2e/ssh-docker-half-open-link.spec.ts', 'tests/e2e/ssh-docker-quick-open-large-listing.spec.ts', 'tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts', diff --git a/tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts b/tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts new file mode 100644 index 00000000000..255014c9b3e --- /dev/null +++ b/tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts @@ -0,0 +1,106 @@ +import { randomUUID } from 'node:crypto' +import { expect, test } from './helpers/orca-app' +import { + cleanupDockerSshRelayTarget, + DOCKER_SSH_RELAY_REMOTE_REPO_PATH, + startDockerSshRelayTarget +} from './helpers/docker-ssh-relay-target' +import { connectDockerSshRelayTarget } from './helpers/docker-ssh-relay-connection' +import { ensureTerminalVisible, waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { + execInTerminal, + focusActiveTerminalInput, + focusLastTerminalPane, + splitActiveTerminalPane, + waitForActivePanePtyId, + waitForActiveTerminalManager, + waitForTerminalOutput +} from './helpers/terminal' +import { readPaneIdentitySnapshot } from './helpers/terminal-pane-identity' +import { quotePosixShell } from '../../src/shared/wsl-login-shell-command' + +function floodWithInputAcknowledgements(marker: string): string { + const script = [ + `const marker=${JSON.stringify(marker)}`, + "const padding='S'.repeat(2048)", + "let input='', ack='', sequence=0, blocked=false", + "process.stdin.setEncoding('utf8')", + "process.stdin.on('data', chunk => { input+=chunk; let end; while((end=input.indexOf('\\n'))>=0) { ack=input.slice(0,end).trim(); input=input.slice(end+1); } })", + "process.stdout.on('drain', () => { blocked=false })", + "setInterval(() => { if(!blocked) blocked=!process.stdout.write(marker+':'+(++sequence)+':ACK='+ack+':'+padding+'\\n'); },8)" + ].join(';') + return `node -e ${quotePosixShell(script)}` +} + +test.describe('five SSH panes under simultaneous output', () => { + test.skip(process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Requires the Docker SSH target') + + test('each pane acknowledges keyboard input after hiding and reopening the flooding workspace', async ({ + orcaPage, + registerPostElectronShutdownCleanup + }, testInfo) => { + test.setTimeout(420_000) + const target = startDockerSshRelayTarget(testInfo) + registerPostElectronShutdownCleanup(async () => cleanupDockerSshRelayTarget(target)) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await connectDockerSshRelayTarget(orcaPage, target, { + remotePath: DOCKER_SSH_RELAY_REMOTE_REPO_PATH + }) + await ensureTerminalVisible(orcaPage, 45_000) + await waitForActiveTerminalManager(orcaPage, 60_000) + const runId = randomUUID() + const owners: { leafId: string; ptyId: string; marker: string }[] = [] + for (let index = 0; index < 5; index++) { + if (index > 0) { + await splitActiveTerminalPane(orcaPage, 'vertical') + await focusLastTerminalPane(orcaPage) + } + const ptyId = await waitForActivePanePtyId(orcaPage, 30_000) + const identity = await readPaneIdentitySnapshot(orcaPage) + expect(identity?.activeLeafId).toBeTruthy() + const marker = `FLOOD_${runId}_${index}` + owners.push({ leafId: identity!.activeLeafId!, ptyId, marker }) + await execInTerminal(orcaPage, ptyId, floodWithInputAcknowledgements(marker)) + await waitForTerminalOutput(orcaPage, `${marker}:`, 60_000, 80_000) + } + expect(new Set(owners.map((owner) => owner.ptyId)).size).toBe(5) + const identity = await readPaneIdentitySnapshot(orcaPage) + expect(identity?.panes).toHaveLength(5) + const tabId = identity!.tabId + const visibleTerminals = orcaPage.locator('.xterm:visible') + await expect(visibleTerminals).toHaveCount(5) + + for (let round = 0; round < 2; round++) { + await orcaPage.evaluate(() => window.__store!.getState().setActiveView('tasks')) + await expect + .poll(() => orcaPage.evaluate(() => window.__store!.getState().activeView)) + .toBe('tasks') + await expect(visibleTerminals).toHaveCount(0) + await orcaPage.evaluate(() => window.__store!.getState().setActiveView('terminal')) + await expect(visibleTerminals).toHaveCount(5) + await waitForActiveTerminalManager(orcaPage, 60_000) + for (const [index, owner] of owners.entries()) { + await orcaPage.evaluate( + ({ tabId, leafId }) => { + const manager = window.__paneManagers!.get(tabId)! + const paneId = manager.getNumericIdForLeaf(leafId) + if (paneId == null) { + throw new Error(`Flood pane ${leafId} did not remount`) + } + manager.setActivePane(paneId, { focus: true }) + }, + { tabId, leafId: owner.leafId } + ) + expect(await waitForActivePanePtyId(orcaPage)).toBe(owner.ptyId) + await focusActiveTerminalInput(orcaPage) + const input = `input_${runId}_${round}_${index}` + await orcaPage.keyboard.type(input) + await orcaPage.keyboard.press('Enter') + // The remote process repeats its latest ACK, so flood eviction cannot hide it. + await waitForTerminalOutput(orcaPage, `:ACK=${input}:`, 30_000, 80_000) + await waitForTerminalOutput(orcaPage, `${owner.marker}:`, 30_000, 80_000) + } + } + }) +})