diff --git a/tests/e2e/terminal-column-probes.ts b/tests/e2e/terminal-column-probes.ts index 88f07a43f02..6e11151658c 100644 --- a/tests/e2e/terminal-column-probes.ts +++ b/tests/e2e/terminal-column-probes.ts @@ -7,6 +7,7 @@ import { waitForPtyPaneMounted, waitForPtyShellEcho } from './terminal-pty-readiness' +import { nodeTerminalCommand } from './terminal-node-command' type TerminalColumnProbeWindow = Window & { __store?: { @@ -87,7 +88,10 @@ export async function waitForPtyColumnsAtMost( await sendToTerminal( page, ptyId, - `node -e ${JSON.stringify(`console.log('${marker}:' + (process.stdout.columns || 0))`)}\r` + `${nodeTerminalCommand([ + '-e', + `console.log('${marker}:' + (process.stdout.columns || 0))` + ])}\r` ) const probeDeadline = Date.now() + Math.min(5_000, Math.max(0, deadline - Date.now())) while (Date.now() < probeDeadline) { diff --git a/tests/e2e/terminal-long-table-scroll-restore.spec.ts b/tests/e2e/terminal-long-table-scroll-restore.spec.ts index 620ab15bea8..3292ea27938 100644 --- a/tests/e2e/terminal-long-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-long-table-scroll-restore.spec.ts @@ -21,6 +21,7 @@ import { waitForPtyColumnsAtMost, waitForRenderedTerminalColumnsAtMost } from './terminal-column-probes' +import { nodeTerminalCommand } from './terminal-node-command' import { waitForPtyShellEcho } from './terminal-pty-readiness' type TerminalRenderDiagnostics = { @@ -620,7 +621,7 @@ test.describe('Terminal long table scroll restore repro', () => { writeFileSync(scriptPath, longMarkdownTableScript(runId)) try { - await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`) + await sendToTerminal(orcaPage, ptyId, `${nodeTerminalCommand([scriptPath])}\r`) await orcaPage.waitForTimeout(80) await switchToWorktree(orcaPage, secondWorktreeId) await waitForActiveTerminalManager(orcaPage, 30_000) @@ -689,7 +690,7 @@ test.describe('Terminal long table scroll restore repro', () => { writeFileSync(scriptPath, narrowSignerMarkdownTableScript(runId)) try { - await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`) + await sendToTerminal(orcaPage, ptyId, `${nodeTerminalCommand([scriptPath])}\r`) await orcaPage.waitForTimeout(80) await switchToWorktree(orcaPage, secondWorktreeId) await waitForActiveTerminalManager(orcaPage, 30_000) @@ -770,7 +771,7 @@ test.describe('Terminal long table scroll restore repro', () => { writeFileSync(scriptPath, emojiFixtureMarkdownTableScript(EMOJI_TABLE_FIXTURE, runId)) try { - await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`) + await sendToTerminal(orcaPage, ptyId, `${nodeTerminalCommand([scriptPath])}\r`) await orcaPage.waitForTimeout(80) await switchToWorktree(orcaPage, secondWorktreeId) await waitForActiveTerminalManager(orcaPage, 30_000) diff --git a/tests/e2e/terminal-node-command.ts b/tests/e2e/terminal-node-command.ts new file mode 100644 index 00000000000..140316497a5 --- /dev/null +++ b/tests/e2e/terminal-node-command.ts @@ -0,0 +1,20 @@ +function quotePosixShellArg(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'` +} + +function quotePowerShellArg(value: string): string { + return `'${value.replaceAll("'", "''")}'` +} + +function quoteTerminalArg(value: string): string { + return process.platform === 'win32' ? quotePowerShellArg(value) : quotePosixShellArg(value) +} + +export function nodeTerminalCommand(args: readonly string[]): string { + const nodeExecutable = quoteTerminalArg(process.execPath) + const executable = process.platform === 'win32' ? `& ${nodeExecutable}` : nodeExecutable + + // Why: Windows CI shells do not always inherit setup-node's PATH, so E2E + // terminal probes must invoke the runner's Node executable directly. + return [executable, ...args.map(quoteTerminalArg)].join(' ') +} diff --git a/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts b/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts index 92e1b9c8197..7bb19138067 100644 --- a/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts +++ b/tests/e2e/terminal-opencode-emoji-table-rendering.spec.ts @@ -15,6 +15,7 @@ import { analyzeRasterCursorCells, type TerminalRasterProbeTarget } from './terminal-cursor-raster-probe' +import { nodeTerminalCommand } from './terminal-node-command' type TerminalRenderState = { coreCursorHidden: boolean | null @@ -270,7 +271,7 @@ test.describe('OpenCode emoji table terminal rendering', () => { const scriptPath = path.join(testRepoPath, `.orca-opencode-emoji-table-${runId}.mjs`) writeFileSync(scriptPath, emojiTableScript(marker)) try { - await sendToTerminal(orcaPage, ptyId, `node ${JSON.stringify(scriptPath)}\r`) + await sendToTerminal(orcaPage, ptyId, `${nodeTerminalCommand([scriptPath])}\r`) await waitForTerminalOutput(orcaPage, marker, 10_000) await orcaPage.waitForTimeout(250) await forceCursorProbeTheme(orcaPage) diff --git a/tests/e2e/terminal-pty-readiness.ts b/tests/e2e/terminal-pty-readiness.ts index e735a6f316c..7991f305339 100644 --- a/tests/e2e/terminal-pty-readiness.ts +++ b/tests/e2e/terminal-pty-readiness.ts @@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto' import type { Page } from '@stablyai/playwright-test' import { expect } from '@stablyai/playwright-test' import { sendToTerminal } from './helpers/terminal' +import { nodeTerminalCommand } from './terminal-node-command' type TerminalPtyReadinessWindow = Window & { __paneManagers?: Map< @@ -67,9 +68,10 @@ export async function waitForPtyPaneMounted( function encodedMarkerCommand(marker: string): string { const encoded = Buffer.from(marker, 'utf8').toString('base64') - return `node -e ${JSON.stringify( + return `${nodeTerminalCommand([ + '-e', `console.log(Buffer.from('${encoded}', 'base64').toString('utf8'))` - )}\r` + ])}\r` } export async function waitForPtyShellEcho( 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 167e1ba8540..043fbe61012 100644 --- a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts @@ -18,6 +18,7 @@ import { waitForTerminalOutput } from './helpers/terminal' import { scrollActiveTerminalToText } from './artificial-opencode-active-terminal-scroll' +import { nodeTerminalCommand } from './terminal-node-command' type BrowserTerminalPane = { terminal: { @@ -522,7 +523,7 @@ test.describe('Terminal raw emoji table scroll restore repro', () => { 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`) + await sendToTerminal(orcaPage, ptyId, `${nodeTerminalCommand([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.