mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
Reply echo suppression modelled two echo shapes from the spec rather than from
a tty. Captured under node-pty against real bash, at a readline prompt and
under `read`:
- Readline mangles CSI replies, not just OSC: `ESC [ ?` becomes BEL and the
residue echoes. The projection was gated on an OSC introducer, so a private
DSR echo was never matched at a readline prompt. This is the reachable one:
a mode-2031 theme push (`CSI ?997;1n`) left latched by an exited TUI paints
`997;1n` on a bash prompt (#9993's scenario).
- ECHOCTL carets EVERY control, not just ESC. A BEL-terminated OSC reply
echoes as `^G`, but the needle kept a literal BEL — a string no tty
produces. Hardening only: every in-tree OSC reply is ST-terminated
(terminal-osc-color-reply.ts:112, xterm's own reply), so the changed byte
is unreachable except from a foreign or older emulator.
Why this is not the CSI projection #13160 review dropped: that one was the
identity (`replaceAll('\x1b]', …)` is a no-op on a CSI reply), so it was
ESC-led and 500ms-held bare-ESC tails away from the query parser. This one is
BEL-led. The rule is now asserted for every shape rather than implied by the
gate: holdPartial iff the needle does not start with ESC.
The readline branch is keyed on the private-DSR grammar with a non-empty
parameter list, plus a floor on needle length. The containment grammar admits
`CSI ? n`, and `answerLiveQueryReply` takes client-supplied bytes on the relay
path, so a peer could otherwise arm a two-byte `BEL n` needle and delete the
first bell-then-`n` in ordinary output. #61c65151129 proved this system can eat
real output when a needle outlives its budget; a length floor is cheap.
Live coverage: pty-reply-echo-shapes.node-pty.test.ts writes a reply to a real
bash master and feeds back what it echoes, so a shell or libc change fails the
suite instead of silently disarming suppression. Registered in the
shell-contracts lane. The transcript tests and the caretEcho helpers that
encoded the same ESC-only assumption are corrected alongside.
Suppression is display-only. This does not change what reaches the child's
stdin — the reply is written to the master either way, in call order.
123 lines
4.4 KiB
TypeScript
123 lines
4.4 KiB
TypeScript
/**
|
|
* Keeps the transcript in pty-startup-reply-echo-shapes.test.ts honest.
|
|
*
|
|
* That file asserts against echo bytes recorded by hand, which is exactly how the two
|
|
* shapes this PR corrected went wrong in the first place: the projection and the test both
|
|
* encoded the same guess. This one writes a reply to a real PTY master, reads what bash
|
|
* actually echoes, and feeds it to the projection — so a shell or libc change that moves
|
|
* the shape fails here instead of silently disarming suppression.
|
|
*
|
|
* Two line disciplines, because they echo differently and both are reachable:
|
|
* readline — tty raw at a prompt, readline echoes in software
|
|
* cooked — under `read`, the kernel echoes via ECHOCTL
|
|
*/
|
|
import { existsSync } from 'node:fs'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { locateEcho, replyEchoProjections } from './pty-startup-reply-echo-shapes'
|
|
import { mode2031SequenceFor } from './terminal-color-scheme-protocol'
|
|
|
|
const BASH = '/bin/bash'
|
|
const itWithBash = process.platform !== 'win32' && existsSync(BASH) ? it : it.skip
|
|
|
|
const COLOR_SCHEME_REPLY = mode2031SequenceFor('dark')
|
|
const OSC_COLOR_REPLY_ST = '\x1b]11;rgb:2e2e/3434/3434\x1b\\'
|
|
|
|
type Pty = { write: (data: string) => void; kill: () => void }
|
|
|
|
let live: Pty | null = null
|
|
|
|
afterEach(() => {
|
|
live?.kill()
|
|
live = null
|
|
})
|
|
|
|
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
|
|
const deadline = Date.now() + timeoutMs
|
|
while (Date.now() < deadline && !predicate()) {
|
|
await sleep(25)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes `reply` to the master once bash is settled, and returns what came back.
|
|
* `discipline: 'cooked'` parks bash in `read` first, which restores ICANON+ECHO.
|
|
*/
|
|
async function echoOf(reply: string, discipline: 'readline' | 'cooked'): Promise<string> {
|
|
const { spawn } = await import('node-pty')
|
|
let output = ''
|
|
const pty = spawn(BASH, ['--norc', '--noprofile', '-i'], {
|
|
name: 'xterm-256color',
|
|
cols: 80,
|
|
rows: 24,
|
|
env: { ...process.env, PS1: 'ORCA16542> ', TERM: 'xterm-256color' }
|
|
})
|
|
live = { write: (data) => pty.write(data), kill: () => pty.kill() }
|
|
pty.onData((data) => {
|
|
output += data
|
|
})
|
|
|
|
await waitFor(() => output.includes('ORCA16542> '), 10_000)
|
|
if (discipline === 'cooked') {
|
|
pty.write('read -r ORCA_LINE\r')
|
|
await sleep(400)
|
|
}
|
|
output = ''
|
|
pty.write(reply)
|
|
// No marker to wait on: the echo is all this produces, so settle instead.
|
|
await waitFor(() => output.length > 0, 5_000)
|
|
await sleep(250)
|
|
return output
|
|
}
|
|
|
|
describe('replyEchoProjections against a real bash pty', () => {
|
|
// The reachable fix: a latched mode-2031 push echoed at a readline prompt had no
|
|
// projection at all before this, so it always painted `997;1n` on the prompt (#9993).
|
|
itWithBash(
|
|
'matches what readline echoes for a mode-2031 reply',
|
|
async () => {
|
|
const echo = await echoOf(COLOR_SCHEME_REPLY, 'readline')
|
|
expect(echo).not.toBe('')
|
|
const match = locateEcho(replyEchoProjections(COLOR_SCHEME_REPLY, 'posix-pty'), echo)
|
|
expect(match).toMatchObject({ kind: 'complete' })
|
|
},
|
|
30_000
|
|
)
|
|
|
|
itWithBash(
|
|
'matches what the kernel echoes for a mode-2031 reply',
|
|
async () => {
|
|
const echo = await echoOf(COLOR_SCHEME_REPLY, 'cooked')
|
|
expect(echo).not.toBe('')
|
|
const match = locateEcho(replyEchoProjections(COLOR_SCHEME_REPLY, 'posix-pty'), echo)
|
|
expect(match).toMatchObject({ kind: 'complete' })
|
|
},
|
|
30_000
|
|
)
|
|
|
|
// The ST form is what every in-tree OSC reply uses, so this is the shape that must never
|
|
// regress — the BEL form is only reachable from a foreign or older emulator.
|
|
itWithBash(
|
|
'matches what the kernel echoes for an ST-terminated OSC reply',
|
|
async () => {
|
|
const echo = await echoOf(OSC_COLOR_REPLY_ST, 'cooked')
|
|
expect(echo).not.toBe('')
|
|
const match = locateEcho(replyEchoProjections(OSC_COLOR_REPLY_ST, 'posix-pty'), echo)
|
|
expect(match).toMatchObject({ kind: 'complete' })
|
|
},
|
|
30_000
|
|
)
|
|
|
|
itWithBash(
|
|
'matches what readline echoes for an ST-terminated OSC reply',
|
|
async () => {
|
|
const echo = await echoOf(OSC_COLOR_REPLY_ST, 'readline')
|
|
expect(echo).not.toBe('')
|
|
const match = locateEcho(replyEchoProjections(OSC_COLOR_REPLY_ST, 'posix-pty'), echo)
|
|
expect(match).toMatchObject({ kind: 'complete' })
|
|
},
|
|
30_000
|
|
)
|
|
})
|