mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 08:02:51 +00:00
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>
134 lines
4.6 KiB
TypeScript
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)
|
|
}
|
|
)
|
|
})
|