Files
orca/src/shared/node-pty-echo-state-requirement.test.ts
T
NeilandBrennan bc319abfbe fix(pty): answer a terminal colour query in its own turn
Root-cause follow-up to #15559, which stopped a CPR overtaking a deferred
colour reply but left the deferral itself in place.

Orca answers terminal queries by writing to the PTY master, which a line
discipline in ECHO copies straight back out as junk on a cooked prompt
(#12112). The guard was to withhold the write until an `stty` subprocess
proved ECHO clear — and forking is what forced the decision to be async.
Any deferral, however short, lets a reply written later in the same turn
overtake this one, so the async probe was the bug's root cause.

Read the bit synchronously instead. Linux and the BSDs redirect a
master's mode ioctls to the slave, so a `tcgetattr` on the master fd
node-pty already owns answers for the slave with no fork: measured 0.26us
against 2403us for the subprocess. With a verdict available inline, a
querying program that already cleared ECHO — every raw-mode prober,
including the colour probe behind the `gh auth login` report — is
answered in its own turn and can never be reordered.

The deferral stays for the genuinely cooked case, and the ordering
guarantee stays underneath it: hosts whose node-pty predates this patch
get no sync probe and fall back to the deferred path, which mixed
client/host versions make a live production path.

Reply routing is all-or-nothing: a payload needing neither containment
nor ordering stays on the host's own path, so a CPR answered during shell
startup cannot pass the daemon's post-ready flush gate and splice into
the buffered startup command.

Native side is fail-safe: a kernel that did not redirect would answer
from the master's own termios, whose ECHO defaults set, so the degraded
verdict is "echoing" — never a false "quiet". The JS half ships in the
pnpm patch while the binding needs a source build, so
ORCA_REQUIRE_NODE_PTY_ECHO_STATE=1 makes CI fail rather than silently
skip when it is handed an upstream prebuild.

Co-authored-by: Brennan <brennanb2025@users.noreply.github.com>
2026-08-19 23:34:30 -07:00

46 lines
2.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
nodePtyEchoStateRequirementViolation,
REQUIRE_NODE_PTY_ECHO_STATE_ENV_VAR,
resolveNodePtyEchoStateSupport
} from './node-pty-echo-state-requirement'
// The exports an upstream node-pty prebuild carries — the shape the guard exists to catch.
const PREBUILD_EXPORTS = { fork: () => {}, open: () => {}, resize: () => {}, process: () => {} }
const SOURCE_BUILD_EXPORTS = { ...PREBUILD_EXPORTS, echoState: () => 0 }
describe('node-pty echoState requirement', () => {
it('accepts a binding that actually exports echoState', () => {
const lookup = resolveNodePtyEchoStateSupport({ native: SOURCE_BUILD_EXPORTS }, 'darwin')
expect(lookup.available).toBe(true)
expect(lookup.nativeExports).toContain('echoState')
})
it('rejects an upstream prebuild whose JS patch would still hand back a probe', () => {
const lookup = resolveNodePtyEchoStateSupport({ native: PREBUILD_EXPORTS }, 'darwin')
expect(lookup.available).toBe(false)
expect(lookup.available ? '' : lookup.reason).toContain('fork, open, resize, process')
// The remedy must name the flag, because plain `pnpm rebuild node-pty` exits 0 on macOS.
expect(lookup.available ? '' : lookup.reason).toContain('npm_config_build_from_source=true')
})
it('rejects a module with no native binding at all', () => {
expect(resolveNodePtyEchoStateSupport({}, 'linux').available).toBe(false)
expect(resolveNodePtyEchoStateSupport(undefined, 'linux').available).toBe(false)
})
it('fails only when CI demanded the source build', () => {
const prebuild = resolveNodePtyEchoStateSupport({ native: PREBUILD_EXPORTS }, 'linux')
const sourceBuild = resolveNodePtyEchoStateSupport({ native: SOURCE_BUILD_EXPORTS }, 'linux')
expect(nodePtyEchoStateRequirementViolation(prebuild, {})).toBeNull()
expect(
nodePtyEchoStateRequirementViolation(prebuild, { [REQUIRE_NODE_PTY_ECHO_STATE_ENV_VAR]: '1' })
).toContain('no echoState')
expect(
nodePtyEchoStateRequirementViolation(sourceBuild, {
[REQUIRE_NODE_PTY_ECHO_STATE_ENV_VAR]: '1'
})
).toBeNull()
})
})