From 44e4d8d4f5c2bde7e549bf7937dc5d6e8c4d8d3d Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:03:24 -0700 Subject: [PATCH] Fix Windows ConPTY probe failures by removing startup control bytes (#5893) * Fix Windows ConPTY probe failures by removing startup control bytes Avoid prepending terminal probes with interrupt (`\x03`) or line-kill (`\x15`) control characters on fresh shell launches. In Windows ConPTY, echoing a startup Ctrl+C can print a literal "^C" and corrupt the subsequent PowerShell commands. - Extract probe command sequence generation to a dedicated helper - Include E2E unit tests in the Vitest test suite config * Rename terminal probe E2E test to match unit test pattern Update the Vitest configuration glob pattern to target only E2E tests ending in `.unit.test.ts`, and rename the terminal probe input sequence test to match. This ensures only unit-like E2E tests are picked up by Vitest. --- config/vitest.config.ts | 7 ++++++- tests/e2e/terminal-column-probes.ts | 16 +++++----------- tests/e2e/terminal-probe-input-sequence.ts | 5 +++++ .../terminal-probe-input-sequence.unit.test.ts | 12 ++++++++++++ tests/e2e/terminal-pty-readiness.ts | 10 ++++------ 5 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/terminal-probe-input-sequence.ts create mode 100644 tests/e2e/terminal-probe-input-sequence.unit.test.ts diff --git a/config/vitest.config.ts b/config/vitest.config.ts index b3846c92cc2..be64fe0c24b 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -15,7 +15,12 @@ export default defineConfig({ }, test: { environment: 'node', - include: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'config/scripts/**/*.test.mjs'], + include: [ + 'src/**/*.test.ts', + 'src/**/*.test.tsx', + 'config/scripts/**/*.test.mjs', + 'tests/e2e/**/*.unit.test.ts' + ], // Why: the full suite runs heavy TS transforms plus real git/http fixtures; // the Vitest 5s defaults are too tight for the slowest integration cases. hookTimeout: 60_000, diff --git a/tests/e2e/terminal-column-probes.ts b/tests/e2e/terminal-column-probes.ts index 6e11151658c..0f8f0400bc7 100644 --- a/tests/e2e/terminal-column-probes.ts +++ b/tests/e2e/terminal-column-probes.ts @@ -8,6 +8,7 @@ import { waitForPtyShellEcho } from './terminal-pty-readiness' import { nodeTerminalCommand } from './terminal-node-command' +import { buildFreshShellProbeInputSequence } from './terminal-probe-input-sequence' type TerminalColumnProbeWindow = Window & { __store?: { @@ -78,21 +79,14 @@ export async function waitForPtyColumnsAtMost( while (Date.now() < deadline) { const marker = `ORCA_PTY_COLUMNS_${randomUUID()}` lastMarker = marker - // Why: a few CI shells occasionally eat the first printable byte when a - // command is written immediately after Ctrl+C/Ctrl+U. Split control bytes - // from the probe command so the shell sees the whole `node` executable. - await sendToTerminal(page, ptyId, '\x03') - await page.waitForTimeout(50) - await sendToTerminal(page, ptyId, '\x15') - await page.waitForTimeout(50) - await sendToTerminal( - page, - ptyId, + for (const input of buildFreshShellProbeInputSequence( `${nodeTerminalCommand([ '-e', `console.log('${marker}:' + (process.stdout.columns || 0))` ])}\r` - ) + )) { + await sendToTerminal(page, ptyId, input) + } const probeDeadline = Date.now() + Math.min(5_000, Math.max(0, deadline - Date.now())) while (Date.now() < probeDeadline) { const content = await getTerminalContentForPtyId(page, ptyId, 30_000) diff --git a/tests/e2e/terminal-probe-input-sequence.ts b/tests/e2e/terminal-probe-input-sequence.ts new file mode 100644 index 00000000000..85402078072 --- /dev/null +++ b/tests/e2e/terminal-probe-input-sequence.ts @@ -0,0 +1,5 @@ +export function buildFreshShellProbeInputSequence(command: string): readonly string[] { + // Why: Windows ConPTY can echo a startup Ctrl+C as literal "^C", which + // corrupts the following PowerShell command before the shell is ready. + return [command] +} diff --git a/tests/e2e/terminal-probe-input-sequence.unit.test.ts b/tests/e2e/terminal-probe-input-sequence.unit.test.ts new file mode 100644 index 00000000000..41e2cd82504 --- /dev/null +++ b/tests/e2e/terminal-probe-input-sequence.unit.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'vitest' +import { buildFreshShellProbeInputSequence } from './terminal-probe-input-sequence' + +describe('buildFreshShellProbeInputSequence', () => { + it('does not prefix fresh shell probes with interrupt or line-kill bytes', () => { + const command = "& 'C:\\node\\node.exe' '-e' 'console.log(1)'\r" + + expect(buildFreshShellProbeInputSequence(command)).toEqual([command]) + expect(buildFreshShellProbeInputSequence(command).join('')).not.toContain('\x03') + expect(buildFreshShellProbeInputSequence(command).join('')).not.toContain('\x15') + }) +}) diff --git a/tests/e2e/terminal-pty-readiness.ts b/tests/e2e/terminal-pty-readiness.ts index 7991f305339..77bc7688346 100644 --- a/tests/e2e/terminal-pty-readiness.ts +++ b/tests/e2e/terminal-pty-readiness.ts @@ -3,6 +3,7 @@ import type { Page } from '@stablyai/playwright-test' import { expect } from '@stablyai/playwright-test' import { sendToTerminal } from './helpers/terminal' import { nodeTerminalCommand } from './terminal-node-command' +import { buildFreshShellProbeInputSequence } from './terminal-probe-input-sequence' type TerminalPtyReadinessWindow = Window & { __paneManagers?: Map< @@ -83,14 +84,11 @@ export async function waitForPtyShellEcho( const deadline = Date.now() + timeoutMs await waitForPtyPaneMounted(page, ptyId, Math.min(10_000, timeoutMs)) while (Date.now() < deadline) { - await sendToTerminal(page, ptyId, '\x03') - await page.waitForTimeout(50) - await sendToTerminal(page, ptyId, '\x15') - await page.waitForTimeout(50) - // Why: terminal scrollback includes command echo. Encode the marker inside // the node snippet so seeing the plain marker proves the shell executed it. - await sendToTerminal(page, ptyId, encodedMarkerCommand(marker)) + for (const input of buildFreshShellProbeInputSequence(encodedMarkerCommand(marker))) { + await sendToTerminal(page, ptyId, input) + } const probeDeadline = Date.now() + Math.min(3_000, Math.max(0, deadline - Date.now())) while (Date.now() < probeDeadline) {