mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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()
|
||||
}
|
||||
})
|
||||
@@ -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!
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user