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.
This commit is contained in:
Jinjing
2026-06-20 02:03:24 -07:00
committed by GitHub
parent a1cbb31f27
commit 44e4d8d4f5
5 changed files with 32 additions and 18 deletions
+6 -1
View File
@@ -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,
+5 -11
View File
@@ -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)
@@ -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]
}
@@ -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')
})
})
+4 -6
View File
@@ -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) {