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) {