diff --git a/tests/e2e/terminal-long-table-scroll-restore.spec.ts b/tests/e2e/terminal-long-table-scroll-restore.spec.ts index b8271f43a4e..620ab15bea8 100644 --- a/tests/e2e/terminal-long-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-long-table-scroll-restore.spec.ts @@ -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') ` } diff --git a/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts b/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts index 56df106998b..92e1b9c8197 100644 --- a/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts +++ b/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts @@ -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`) diff --git a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts index be4fa51618f..167e1ba8540 100644 --- a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts @@ -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 { 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)