Files
orca/src/main/codex/codex-real-home-path.test.ts
T
NeilandOrca 5ea7df1a5b fix(terminal): make DECSET 2031 subscriptions silent (#13904)
fish arms `CSI ?2031h` before painting each prompt and withdraws it when it
hands the tty to a child — a ~1ms window. Orca answered that subscribe with
`CSI ?997;Nn` across a 1-3ms renderer hop, so the reply landed after the
withdrawal and was read as stdin by the next child, corrupting `brew`/`npx`
`[y/N]` prompts.

The reply is not stale by Orca's own view when written (measured
staleReplies: 0), so no suppress-the-stale-reply scheme can close this — the
information needed to suppress does not exist yet. Nothing asked for the reply
either. The Contour spec says a terminal "should only send out the DSR when the
palette has been updated"; Ghostty (Termio.zig:729 — force=true reachable only
from the ?996n DSR), iTerm2 (VT100Terminal.m:995 — flag only) and xterm.js
(InputHandler.ts:2035 — flag only) all emit nothing on the DECSET. So stop
entering the race: record the subscription, answer nothing.

Of 17 real programs measured under a pty, only fish, tmux, claude and opencode
subscribe; none block on a reply, and answering produces one redundant palette
re-query and zero rendering difference. tmux is the only one that sends `?996n`,
which Orca still answers.

- Subscribes are record-only at all four emitters (live scan, hidden-gate fact,
  parked byte watcher, parked responder — the last is deleted, it only replied).
- `?996n` answers, the subscription registry, and the theme-flip push are
  unchanged. `paneLastThemeMode` is still seeded at subscribe so the next
  appearance re-apply is not read as a flip.
- Replay grammar carries `?2031l` alongside `?2031h`, so a late-attaching remote
  client no longer registers a subscription the TUI already retired.

Also closes fish-integration gaps found alongside: `unset` (which fish lacks)
becomes `set -e` on paths parsed by the client's login shell, `config.fish` is
parsed for agent-home detection, and bracketed-paste startup delivery is made
consistent across local/daemon/relay.

Regression test drives real fish 4.7.1 under node-pty and asserts on what the
child process reads; it fails against pre-fix code with the exact payload from
the issue. CI installs fish 4 and fails loudly rather than skipping.

Closes #9993

Co-authored-by: Orca <help@stably.ai>
2026-08-11 21:16:36 -07:00

134 lines
4.6 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, sep } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
getCustomCodexHomeOverrideForLaunch,
hasCustomCodexHomeOverride,
hasCustomCodexHomeOverrideForLaunch,
shellStartupCodexHomeOverrideMatches
} from './codex-real-home-path'
import { __resetShellStartupEnvCache } from '../pty/shell-startup-env'
const temporaryHomes: string[] = []
const savedConfigHome = process.env.XDG_CONFIG_HOME
afterEach(() => {
__resetShellStartupEnvCache()
if (savedConfigHome === undefined) {
delete process.env.XDG_CONFIG_HOME
} else {
process.env.XDG_CONFIG_HOME = savedConfigHome
}
for (const path of temporaryHomes.splice(0)) {
rmSync(path, { recursive: true, force: true })
}
})
describe('hasCustomCodexHomeOverride', () => {
it('recognizes normalized aliases of Orca-owned CODEX_HOME', () => {
const managedHome = `${process.cwd()}${sep}codex-runtime-home${sep}home`
expect(
hasCustomCodexHomeOverride({
CODEX_HOME: `${managedHome}${sep}.`,
ORCA_CODEX_HOME: managedHome
})
).toBe(false)
})
it('preserves a genuinely custom CODEX_HOME', () => {
expect(
hasCustomCodexHomeOverride({
CODEX_HOME: `${process.cwd()}${sep}custom-codex-home`,
ORCA_CODEX_HOME: `${process.cwd()}${sep}codex-runtime-home${sep}home`
})
).toBe(true)
})
it('captures explicit environment provenance for restart comparison', () => {
const codexHome = join(process.cwd(), 'custom-codex-home')
expect(getCustomCodexHomeOverrideForLaunch({ CODEX_HOME: codexHome })).toEqual({
source: 'environment',
context: { codexHome }
})
})
it.skipIf(process.platform === 'win32')(
'detects a pane-local shell startup override from its launch HOME',
() => {
const paneHome = mkdtempSync(join(tmpdir(), 'orca-codex-pane-home-'))
temporaryHomes.push(paneHome)
writeFileSync(join(paneHome, '.zshrc'), 'export CODEX_HOME="$HOME/custom-codex-home"\n')
// Why cleared: the context records XDG_CONFIG_HOME, so a developer machine
// that sets one would otherwise change the recorded shape.
delete process.env.XDG_CONFIG_HOME
expect(hasCustomCodexHomeOverrideForLaunch({ HOME: paneHome, SHELL: '/bin/zsh' })).toBe(true)
const override = getCustomCodexHomeOverrideForLaunch({
HOME: paneHome,
SHELL: '/bin/zsh'
})
expect(override).toEqual({
source: 'shell-startup',
context: {
home: paneHome,
shell: '/bin/zsh',
codexHome: join(paneHome, 'custom-codex-home')
}
})
expect(
override?.source === 'shell-startup' &&
shellStartupCodexHomeOverrideMatches(override.context)
).toBe(true)
}
)
// Why: a fish user who exports XDG_CONFIG_HOME from config.fish never passes it to
// a Dock-launched Orca, so the launch env is the only place it appears. Reading the
// main process env instead scans ~/.config and misses the override entirely.
it.skipIf(process.platform === 'win32')(
'resolves a fish override under the launch env XDG_CONFIG_HOME, not the process one',
() => {
const paneHome = mkdtempSync(join(tmpdir(), 'orca-codex-fish-home-'))
temporaryHomes.push(paneHome)
const configHome = join(paneHome, 'xdg')
mkdirSync(join(configHome, 'fish'), { recursive: true })
writeFileSync(
join(configHome, 'fish', 'config.fish'),
'set -gx CODEX_HOME "$HOME/custom-codex-home"\n'
)
// The decoy fish reads only if XDG_CONFIG_HOME is ignored.
mkdirSync(join(paneHome, '.config', 'fish'), { recursive: true })
writeFileSync(
join(paneHome, '.config', 'fish', 'config.fish'),
'set -gx CODEX_HOME /wrong-default-config-home\n'
)
delete process.env.XDG_CONFIG_HOME
const launchEnv = {
HOME: paneHome,
SHELL: '/opt/homebrew/bin/fish',
XDG_CONFIG_HOME: configHome
}
const override = getCustomCodexHomeOverrideForLaunch(launchEnv)
expect(override).toEqual({
source: 'shell-startup',
context: {
home: paneHome,
shell: '/opt/homebrew/bin/fish',
configHome,
codexHome: join(paneHome, 'custom-codex-home')
}
})
// And the recorded configHome is what a later re-check resolves against.
expect(
override?.source === 'shell-startup' &&
shellStartupCodexHomeOverrideMatches(override.context)
).toBe(true)
}
)
})