test(e2e): keys typed into a new remote terminal before its first prompt reach the shell (#35312)

* test(e2e): keys typed into a new remote terminal before its first prompt reach the shell

Add a headless paired-host spec that types into each new remote terminal after
it binds while the host shell's rc file is still running, then requires the
whole command to run. Drop the managed-SSH helper's wait for a prompt before
typing, so those specs assert the same guarantee.

* test(e2e): prove the rc gate held, and that the early-typed command ran exactly once

The tty echo of the typed command must precede the rc file's own prompt, the
command's output must appear exactly once, and the rc wait outlasts the bind
wait. Throw if the host has no isolated HOME.

* test(e2e): drop the early-typed echo-position check; keep the gate and exactly-once proofs

The check that the typed line's tty echo precedes the rc file's prompt failed on
Linux CI 2 of 2 (run 40763469331): the pane began with the prompt and the typed
line followed it, while the command still ran exactly once. macOS echoes it during
the rc. Where the echo lands depends on when the host's line discipline sees the
bytes, which this test cannot pin; it is not evidence of loss. What the test keeps
proving: the keys were typed while the rc file still waited (release file absent),
the rc ran (its own PROMPT appears), and the command ran whole, exactly once.

* test(e2e): name the early-typing test for what it proves

Rename it to say every key typed before the first prompt is delivered exactly
once, and drop the header's claim about how the tty keeps type-ahead.
This commit is contained in:
Neil
2026-10-28 14:43:36 -07:00
committed by GitHub
parent 7375d3abd5
commit b47f12abd5
2 changed files with 127 additions and 7 deletions
@@ -0,0 +1,126 @@
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import type { Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host'
import { launchPairedWebClient, type PairedWebClient } from './helpers/paired-electron-client'
import { focusActiveTerminalInput } from './helpers/terminal'
const ROUNDS = 3
const PROMPT = 'EARLY_TYPING_PROMPT$ '
async function terminalTabIds(page: Page, worktreeId: string): Promise<string[]> {
return page.evaluate(
(id) => (window.__store?.getState().tabsByWorktree[id] ?? []).map((tab) => tab.id),
worktreeId
)
}
function paneText(page: Page, tabId: string): Promise<string> {
return page.evaluate((id) => {
const manager = window.__paneManagers?.get(id)
const buffer = (manager?.getActivePane?.() ?? manager?.getPanes?.()[0])?.terminal?.buffer.active
if (!buffer) {
return ''
}
const lines: string[] = []
for (let row = 0; row < buffer.length; row += 1) {
lines.push(buffer.getLine(row)?.translateToString(true) ?? '')
}
return lines.join('\n')
}, tabId)
}
function paneIsBound(page: Page, tabId: string): Promise<boolean> {
return page.evaluate((id) => {
const manager = window.__paneManagers?.get(id)
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0]
return Boolean(pane?.container?.dataset?.ptyId)
}, tabId)
}
// Keys typed into a new remote terminal after it binds, while the host shell's rc file still runs,
// must all reach that shell: nothing may drop a prefix or deliver them twice.
test("keys typed before a new remote terminal's first prompt are all delivered, exactly once", async ({
testRepoPath
}) => {
// Three host terminals, each held at its rc file until the keys are in, plus host and client boot.
test.setTimeout(240_000)
const host = await launchHeadlessPairedRuntimeHost({ extraEnv: { SHELL: '/bin/bash' } })
const home = host.env.HOME
if (!home) {
throw new Error('The headless host launched without an isolated HOME')
}
const release = path.join(home, '.orca-e2e-release-rc')
mkdirSync(home, { recursive: true })
// Each shell waits in its rc file for the release file (bounded at ~120s, past the 60s bind wait).
const rc = [
'for _ in $(seq 2400); do [ -f "$HOME/.orca-e2e-release-rc" ] && break; sleep 0.05; done',
`PS1='${PROMPT}'`
].join('\n')
writeFileSync(path.join(home, '.bash_profile'), `${rc}\n`)
// Released until the rounds start: the host's own startup shell probes read this rc file too.
writeFileSync(release, '')
let client: PairedWebClient | null = null
try {
await host.client.call('repo.add', { path: testRepoPath, kind: 'git' })
client = await launchPairedWebClient(host.app, host.offer)
const page = client.page
await expect
.poll(() => page.evaluate(() => window.__store?.getState().allWorktrees()[0]?.id ?? null), {
timeout: 30_000
})
.not.toBeNull()
const worktreeId = await page.evaluate(
() => window.__store?.getState().allWorktrees()[0]?.id ?? ''
)
await page.evaluate((id) => {
const state = window.__store!.getState()
state.setActiveView('terminal')
state.setActiveWorktree(id)
}, worktreeId)
for (let round = 0; round < ROUNDS; round += 1) {
rmSync(release, { force: true })
const before = new Set(await terminalTabIds(page, worktreeId))
await page.evaluate(async (id) => {
const state = window.__store!.getState()
await state.openNewTerminalTabInActiveWorkspace(state.activeGroupIdByWorktree[id]!)
}, worktreeId)
let tabId = ''
await expect
.poll(async () => {
tabId = (await terminalTabIds(page, worktreeId)).find((id) => !before.has(id)) ?? ''
return tabId
})
.not.toBe('')
// Type into this tab even if the worktree's own first terminal opened alongside it.
await page.evaluate((id) => window.__store!.getState().setActiveTab(id), tabId)
await expect.poll(() => paneIsBound(page, tabId), { timeout: 60_000 }).toBe(true)
expect(await paneText(page, tabId)).not.toContain(PROMPT)
await focusActiveTerminalInput(page)
const typed = `echo EARLY_TYPING_${round}_$((40+2))`
await page.keyboard.insertText(typed)
await page.keyboard.press('Enter')
expect(existsSync(release)).toBe(false)
writeFileSync(release, '')
const ran = new RegExp(`^EARLY_TYPING_${round}_42$`, 'gm')
await expect
.poll(async () => (await paneText(page, tabId)).match(ran)?.length ?? 0, {
timeout: 30_000,
message: `round ${round}: the shell never ran the whole early-typed command`
})
.toBeGreaterThan(0)
const text = await paneText(page, tabId)
// The rc file's own prompt proves it ran. Where the typed line's echo lands is not asserted:
// it varies by host (macOS echoes it during the rc; Linux CI shows it only after the prompt).
expect(text).toContain(PROMPT)
expect(text.match(ran)).toHaveLength(1)
}
} finally {
await client?.dispose()
await host.dispose()
}
})
+1 -7
View File
@@ -185,13 +185,7 @@ export async function openTerminalTab(page: Page, worktreeId: string): Promise<s
)
.not.toBeNull()
await waitForPane(page, created!)
// Keys typed before the shell's first prompt can be lost, as they would be for a user too.
await expect
.poll(() => readPairedPaneContent(page, created!), {
timeout: 60_000,
message: `the shell in ${created} never printed a prompt`
})
.toMatch(/[#$] /)
// No prompt wait: keys typed before the first prompt must reach the shell, as for a user.
return created!
}