Files
orca/src/shared/startup-command-submission.test.ts
T
3d7b306183 fix(terminal): submit startup commands with Enter on every platform (#23672)
* fix(terminal): submit startup commands with Enter on every platform

LF is Ctrl+J to a line editor, so a shell that remaps it left the command at the prompt.

Fixes #23250

Co-authored-by: Aashish Mahato <145881415+aashish254@users.noreply.github.com>

* test(terminal): expect CR in pty ipc startup tests

* fix(terminal): submit every line of a raw multiline startup command with CR

The CR switch only replaced the trailing terminator. On the raw path — a shell
with no bracketed paste, so each line break submits its own line — interior LFs
stayed LF, which is the same remappable Ctrl+J the fix set out to remove. A
multiline startup command in sh/dash/nushell/PowerShell therefore still stalled
partway through for a user who rebinds Ctrl+J. Normalise interior breaks to CR
on that path only; the bracketed-paste payload keeps its literal newlines.

* fix(terminal): quote multi-line PowerShell arguments on one line

A multi-line agent prompt was quoted as a single-quoted PowerShell string
with raw line breaks. Typed into Windows PowerShell 5.1 without PSReadLine,
each break submits a line and the shell waits at `>>` forever. Values with
CR/LF now become one double-quoted line with backtick escapes, keeping every
byte; single-line values keep the single-quoted form unchanged.

* style(terminal): keep typographic quotes as \u escapes in PowerShell quoting

Restores the escape form the single-quoted regex used on main and applies
it to the new double-quoted regex and the native argv test, so the diff
against main stays minimal and the characters are visible in review.

---------

Co-authored-by: Aashish Mahato <145881415+aashish254@users.noreply.github.com>
Co-authored-by: Neil <neil@stably.ai>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-10-05 16:20:15 -07:00

75 lines
2.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
buildStartupCommandSubmission,
isBracketedPasteSafeShell
} from './startup-command-submission'
describe('buildStartupCommandSubmission', () => {
it('submits a single-line command with CR', () => {
expect(buildStartupCommandSubmission('claude', { bracketedPasteSafe: true })).toBe('claude\r')
expect(buildStartupCommandSubmission('claude', { bracketedPasteSafe: false })).toBe('claude\r')
})
it('replaces a caller-supplied trailing terminator with CR', () => {
for (const command of ['claude\n', 'claude\r', 'claude\r\n']) {
expect(buildStartupCommandSubmission(command, { bracketedPasteSafe: true })).toBe('claude\r')
}
})
it('wraps a multiline command in bracketed paste with a trailing submit byte', () => {
const command = "claude 'first\nsecond'"
expect(buildStartupCommandSubmission(command, { bracketedPasteSafe: true })).toBe(
`\x1b[200~${command}\x1b[201~\r`
)
})
it('strips a trailing submit byte before bracket-wrapping the multiline body', () => {
const body = "claude 'first\nsecond'"
expect(buildStartupCommandSubmission(`${body}\n`, { bracketedPasteSafe: true })).toBe(
`\x1b[200~${body}\x1b[201~\r`
)
})
it('keeps the raw path for multiline commands when bracketed paste is unsafe', () => {
expect(buildStartupCommandSubmission('echo one\necho two', { bracketedPasteSafe: false })).toBe(
'echo one\recho two\r'
)
})
// Why: on the raw path every line break submits a line, so an interior LF is the same
// remappable Ctrl+J the trailing terminator was (#23250).
it('submits every line of a raw multiline command with CR', () => {
for (const command of ['echo one\necho two', 'echo one\r\necho two', 'echo one\recho two']) {
expect(buildStartupCommandSubmission(command, { bracketedPasteSafe: false })).toBe(
'echo one\recho two\r'
)
}
})
it('leaves interior line breaks intact inside a bracketed-paste payload', () => {
const command = "claude 'first\nsecond'"
expect(buildStartupCommandSubmission(command, { bracketedPasteSafe: true })).toBe(
`\x1b[200~${command}\x1b[201~\r`
)
})
})
describe('isBracketedPasteSafeShell', () => {
it('always trusts bash and zsh', () => {
for (const shellName of ['bash', 'zsh', 'BASH']) {
expect(isBracketedPasteSafeShell({ shellName, waitsForShellReady: false })).toBe(true)
}
})
it('trusts fish only behind the shell-ready barrier', () => {
expect(isBracketedPasteSafeShell({ shellName: 'fish', waitsForShellReady: true })).toBe(true)
expect(isBracketedPasteSafeShell({ shellName: 'fish', waitsForShellReady: false })).toBe(false)
})
it('rejects shells with no known bracketed-paste line editor', () => {
for (const shellName of ['sh', 'dash', 'nu', 'powershell.exe', '']) {
expect(isBracketedPasteSafeShell({ shellName, waitsForShellReady: true })).toBe(false)
}
})
})