mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Stabilize terminal e2e tests on Windows ConPTY (#5881)
* Stabilize terminal e2e tests on Windows ConPTY Windows ConPTY can drop or reorder the tail of large synchronized terminal frames or stdout writes when they are written all at once or when the process exits too quickly. * Wait for PTY shell readiness before running test scripts. * Chunk large table writes line-by-line and yield with a short timeout on Windows to let the stream drain. * Add and poll for a synchronized frame tail marker to ensure the entire content is rendered before proceeding. * Remove flaky preflight shell echo in terminal emoji scroll test The Ctrl+C/Ctrl+U preflight keys sent by `waitForPtyShellEcho` can race Windows ConPTY startup and eat subsequent test input. Remove this wait and rely on the fixture marker as the readiness signal instead.
This commit is contained in:
@@ -126,18 +126,16 @@ outputRows.push('|-' + widths.map((width) => '-'.repeat(width)).join('-|-') + '-
|
||||
for (let repeat = 0; repeat < 4; repeat += 1) {
|
||||
for (const row of rows) outputRows.push(line(row))
|
||||
}
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
let index = 0
|
||||
const timer = setInterval(() => {
|
||||
if (index < outputRows.length) {
|
||||
process.stdout.write(outputRows[index] + '\\n')
|
||||
index += 1
|
||||
return
|
||||
}
|
||||
clearInterval(timer)
|
||||
process.stdout.write('LONG_TABLE_SCROLL_RESTORE_${runId}\\n')
|
||||
process.stdout.write('\\x1b[?2026l')
|
||||
}, 8)
|
||||
async function writeStdout(chunk) {
|
||||
await new Promise((resolve) => process.stdout.write(chunk, resolve))
|
||||
if (process.platform === 'win32') await new Promise((resolve) => setTimeout(resolve, 8))
|
||||
}
|
||||
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
for (const row of outputRows) {
|
||||
await writeStdout(row + '\\n')
|
||||
}
|
||||
await writeStdout('\\x1b[?2026l')
|
||||
await writeStdout('LONG_TABLE_SCROLL_RESTORE_${runId}\\n')
|
||||
`
|
||||
}
|
||||
|
||||
@@ -250,6 +248,10 @@ function renderRow(cells) {
|
||||
}
|
||||
return rows
|
||||
}
|
||||
async function writeStdout(chunk) {
|
||||
await new Promise((resolve) => process.stdout.write(chunk, resolve))
|
||||
if (process.platform === 'win32') await new Promise((resolve) => setTimeout(resolve, 8))
|
||||
}
|
||||
const parsedRows = table
|
||||
.split(/\\r?\\n/)
|
||||
.filter((row) => row.trim().startsWith('|') && !isSeparatorRow(row))
|
||||
@@ -259,12 +261,13 @@ for (const [index, row] of parsedRows.entries()) {
|
||||
rendered.push(...renderRow(row))
|
||||
rendered.push(rule(index === parsedRows.length - 1 ? border.bottom : border.middle))
|
||||
}
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
process.stdout.write(rendered.join('\\r\\n'))
|
||||
process.stdout.write('\\r\\n')
|
||||
process.stdout.write('\\r\\n${widthMarker}:' + generatedTableWidth + '\\r\\n')
|
||||
process.stdout.write('\\r\\n${marker}\\r\\n')
|
||||
process.stdout.write('\\x1b[?2026l')
|
||||
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
for (const line of rendered) {
|
||||
await writeStdout(line + '\\r\\n')
|
||||
}
|
||||
await writeStdout('\\x1b[?2026l')
|
||||
await writeStdout('${widthMarker}:' + generatedTableWidth + '\\r\\n')
|
||||
await writeStdout('${marker}\\r\\n')
|
||||
`
|
||||
}
|
||||
|
||||
@@ -286,12 +289,16 @@ function narrowSignerMarkdownTableScript(runId: string): string {
|
||||
).flat()
|
||||
return `
|
||||
const rows = ${JSON.stringify(repeatedRows)}
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
for (const row of rows) {
|
||||
process.stdout.write(row + '\\r\\n')
|
||||
async function writeStdout(chunk) {
|
||||
await new Promise((resolve) => process.stdout.write(chunk, resolve))
|
||||
if (process.platform === 'win32') await new Promise((resolve) => setTimeout(resolve, 8))
|
||||
}
|
||||
process.stdout.write('${marker}\\r\\n')
|
||||
process.stdout.write('\\x1b[?2026l')
|
||||
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
for (const row of rows) {
|
||||
await writeStdout(row + '\\r\\n')
|
||||
}
|
||||
await writeStdout('\\x1b[?2026l')
|
||||
await writeStdout('${marker}\\r\\n')
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
waitForActiveTerminalManager,
|
||||
waitForTerminalOutput
|
||||
} from './helpers/terminal'
|
||||
import { waitForPtyShellEcho } from './terminal-pty-readiness'
|
||||
import {
|
||||
analyzeRasterCursorCells,
|
||||
type TerminalRasterProbeTarget
|
||||
@@ -59,11 +60,20 @@ function emojiTableScript(marker: string): string {
|
||||
].join('\r\n')
|
||||
|
||||
return `
|
||||
process.stdout.write('\\x1b[?2026h\\x1b[?25l')
|
||||
process.stdout.write('\\x1b[2J\\x1b[H')
|
||||
process.stdout.write(${JSON.stringify(table)})
|
||||
process.stdout.write('\\r\\n${marker}\\r\\n')
|
||||
process.stdout.write('\\x1b[?25h\\x1b[?2026l')
|
||||
async function writeStdout(chunk) {
|
||||
await new Promise((resolve) => {
|
||||
process.stdout.write(chunk, resolve)
|
||||
})
|
||||
if (process.platform === 'win32') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 8))
|
||||
}
|
||||
}
|
||||
await writeStdout('\\x1b[?2026h\\x1b[?25l\\x1b[2J\\x1b[H')
|
||||
for (const line of ${JSON.stringify(table.split('\r\n'))}) {
|
||||
await writeStdout(line + '\\r\\n')
|
||||
}
|
||||
await writeStdout('\\x1b[?25h\\x1b[?2026l')
|
||||
await writeStdout('${marker}\\r\\n')
|
||||
setTimeout(() => process.exit(0), 50)
|
||||
`
|
||||
}
|
||||
@@ -254,6 +264,7 @@ test.describe('OpenCode emoji table terminal rendering', () => {
|
||||
await enableRiskyTerminalRendererPath(orcaPage)
|
||||
|
||||
const ptyId = await waitForActivePanePtyId(orcaPage)
|
||||
await waitForPtyShellEcho(orcaPage, ptyId, 20_000)
|
||||
const runId = randomUUID()
|
||||
const marker = `${EMOJI_TABLE_MARKER}_${runId}`
|
||||
const scriptPath = path.join(testRepoPath, `.orca-opencode-emoji-table-${runId}.mjs`)
|
||||
|
||||
@@ -62,6 +62,7 @@ const RAW_EMOJI_BOX_TABLE_WIDTH =
|
||||
|
||||
function rawEmojiFixtureBoxTableScript(table: string, runId: string): string {
|
||||
const marker = `RAW_EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
||||
const frameTailMarker = rawEmojiFixtureFrameTailMarker(runId)
|
||||
return `
|
||||
const table = ${JSON.stringify(table)}
|
||||
const widths = ${JSON.stringify(RAW_EMOJI_BOX_TABLE_COLUMN_WIDTHS)}
|
||||
@@ -131,12 +132,23 @@ function renderRow(cells) {
|
||||
border.vertical
|
||||
)
|
||||
}
|
||||
// Why: Windows can lose the tail of large stdout writes if this fixture exits
|
||||
// immediately, which hides the completion marker behind a shell prompt.
|
||||
function writeStdout(chunk) {
|
||||
return new Promise((resolve) => {
|
||||
// Why: Windows ConPTY can drop or reorder the tail of one large synchronized
|
||||
// frame. Drain and yield between chunks so the golden waits on transport, not luck.
|
||||
async function writeStdout(chunk) {
|
||||
await new Promise((resolve) => {
|
||||
process.stdout.write(chunk, resolve)
|
||||
})
|
||||
if (process.platform === 'win32') {
|
||||
await new Promise((resolve) => setTimeout(resolve, 8))
|
||||
}
|
||||
}
|
||||
async function writeStdoutLine(line) {
|
||||
await writeStdout(line + '\\r\\n')
|
||||
}
|
||||
async function flushStdout() {
|
||||
return new Promise((resolve) => {
|
||||
process.stdout.write('', resolve)
|
||||
})
|
||||
}
|
||||
const parsedRows = table
|
||||
.split(/\\r?\\n/)
|
||||
@@ -148,9 +160,13 @@ for (const [index, row] of parsedRows.entries()) {
|
||||
rendered.push(rule(index === parsedRows.length - 1 ? border.bottom : border.middle))
|
||||
}
|
||||
await writeStdout('\\x1b[?2026h\\x1b[2J\\x1b[H')
|
||||
await writeStdout(rendered.join('\\r\\n'))
|
||||
await writeStdout('\\r\\n${marker}\\r\\n')
|
||||
for (const line of rendered) {
|
||||
await writeStdoutLine(line)
|
||||
}
|
||||
await writeStdoutLine('${frameTailMarker}')
|
||||
await writeStdout('\\x1b[?2026l')
|
||||
await writeStdoutLine('${marker}')
|
||||
await flushStdout()
|
||||
`
|
||||
}
|
||||
|
||||
@@ -158,6 +174,10 @@ function rawEmojiFixtureCompletionMarker(runId: string): string {
|
||||
return `RAW_EMOJI_FIXTURE_TABLE_RESTORE_${runId}`
|
||||
}
|
||||
|
||||
function rawEmojiFixtureFrameTailMarker(runId: string): string {
|
||||
return `RAW_EMOJI_FIXTURE_TABLE_FRAME_TAIL_${runId}`
|
||||
}
|
||||
|
||||
async function setWideRenderedTableViewport(page: Page): Promise<void> {
|
||||
await page.setViewportSize({ width: 1480, height: 820 })
|
||||
await page.waitForTimeout(250)
|
||||
@@ -499,11 +519,20 @@ test.describe('Terminal raw emoji table scroll restore repro', () => {
|
||||
|
||||
try {
|
||||
const completionMarker = rawEmojiFixtureCompletionMarker(runId)
|
||||
const frameTailMarker = rawEmojiFixtureFrameTailMarker(runId)
|
||||
// Why: the fixture marker is the shell-readiness signal here; an extra
|
||||
// Ctrl+C/Ctrl+U preflight can race Windows ConPTY startup and eat input.
|
||||
await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`)
|
||||
// Why: Windows ConPTY can return the PowerShell prompt while xterm is
|
||||
// still flushing synchronized output if the pane is hidden immediately.
|
||||
// This golden is about restored table geometry, not shell-flush timing.
|
||||
await waitForTerminalOutput(orcaPage, completionMarker, 20_000, 30_000)
|
||||
await expect
|
||||
.poll(() => getTerminalContent(orcaPage, 30_000), {
|
||||
timeout: 10_000,
|
||||
message: 'raw emoji table synchronized frame tail was not rendered'
|
||||
})
|
||||
.toContain(frameTailMarker)
|
||||
await switchToWorktree(orcaPage, secondWorktreeId)
|
||||
await waitForActiveTerminalManager(orcaPage, 30_000)
|
||||
await orcaPage.waitForTimeout(1_000)
|
||||
|
||||
Reference in New Issue
Block a user