Files
orca/src/relay/plugin-overlay-env.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

135 lines
4.4 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { __resetShellStartupEnvCache } from '../main/pty/shell-startup-env'
import { resolveOpenCodeSourceConfigDir, resolvePiSourceAgentDir } from './plugin-overlay-env'
describe('plugin overlay env source resolution', () => {
let homeDir: string
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), 'relay-plugin-overlay-env-'))
__resetShellStartupEnvCache()
})
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true })
__resetShellStartupEnvCache()
})
it.skipIf(process.platform === 'win32')(
'uses zsh startup exports before inherited public overlay env',
() => {
mkdirSync(join(homeDir, 'company-opencode'), { recursive: true })
mkdirSync(join(homeDir, 'company-pi'), { recursive: true })
writeFileSync(
join(homeDir, '.zshrc'),
[
'export OPENCODE_CONFIG_DIR="$HOME/company-opencode"',
'export PI_CODING_AGENT_DIR="$HOME/company-pi"'
].join('\n')
)
const env = {
HOME: homeDir,
OPENCODE_CONFIG_DIR: '/tmp/inherited-opencode-overlay',
PI_CODING_AGENT_DIR: '/tmp/inherited-pi-overlay'
}
expect(resolveOpenCodeSourceConfigDir(env, '/bin/zsh')).toBe(
join(homeDir, 'company-opencode')
)
expect(resolvePiSourceAgentDir(env, '/bin/zsh', 'pi')).toBe(join(homeDir, 'company-pi'))
}
)
it.skipIf(process.platform === 'win32')(
'discovers overlay sources from a custom zsh ZDOTDIR',
() => {
const zshDir = join(homeDir, '.config', 'zsh')
mkdirSync(zshDir, { recursive: true })
writeFileSync(join(homeDir, '.zshenv'), 'export ZDOTDIR="$HOME/.config/zsh"\n')
writeFileSync(join(zshDir, '.zshrc'), 'export OPENCODE_CONFIG_DIR="$HOME/opencode-src"\n')
expect(
resolveOpenCodeSourceConfigDir(
{
HOME: homeDir,
OPENCODE_CONFIG_DIR: '/tmp/inherited-opencode-overlay'
},
'/bin/zsh'
)
).toBe(join(homeDir, 'opencode-src'))
}
)
it('keeps explicit original-source env ahead of startup hints', () => {
writeFileSync(join(homeDir, '.zshrc'), 'export OPENCODE_CONFIG_DIR="$HOME/company-opencode"\n')
expect(
resolveOpenCodeSourceConfigDir(
{
HOME: homeDir,
ORCA_OPENCODE_SOURCE_CONFIG_DIR: '/remote/original-opencode',
OPENCODE_CONFIG_DIR: '/tmp/inherited-opencode-overlay'
},
'/bin/zsh'
)
).toBe('/remote/original-opencode')
})
it.skipIf(process.platform === 'win32')('resolves Prime from its independent env keys', () => {
writeFileSync(
join(homeDir, '.zshrc'),
'export PRIME_AGENT_CODING_AGENT_DIR="$HOME/company-prime"\n'
)
expect(
resolvePiSourceAgentDir(
{ HOME: homeDir, PRIME_AGENT_CODING_AGENT_DIR: '/tmp/inherited-prime' },
'/bin/zsh',
'prime-agent'
)
).toBe(join(homeDir, 'company-prime'))
expect(
resolvePiSourceAgentDir(
{
HOME: homeDir,
ORCA_PRIME_AGENT_SOURCE_AGENT_DIR: '/remote/original-prime',
PRIME_AGENT_CODING_AGENT_DIR: '/tmp/inherited-prime'
},
'/bin/zsh',
'prime-agent'
)
).toBe('/remote/original-prime')
})
// Why: the session env is the only place a fish user's XDG_CONFIG_HOME shows up
// (config.fish exports it, so no GUI-launched process inherits it). Dropping it
// here would scan ~/.config and disagree with the same lookup on the main side.
it.skipIf(process.platform === 'win32')(
'reads fish config under the session XDG_CONFIG_HOME',
() => {
const configHome = join(homeDir, 'xdg')
mkdirSync(join(configHome, 'fish'), { recursive: true })
writeFileSync(
join(configHome, 'fish', 'config.fish'),
'set -gx OPENCODE_CONFIG_DIR "$HOME/company-opencode"\n'
)
mkdirSync(join(homeDir, '.config', 'fish'), { recursive: true })
writeFileSync(
join(homeDir, '.config', 'fish', 'config.fish'),
'set -gx OPENCODE_CONFIG_DIR /wrong-default-config-home\n'
)
expect(
resolveOpenCodeSourceConfigDir(
{ HOME: homeDir, XDG_CONFIG_HOME: configHome },
'/opt/homebrew/bin/fish'
)
).toBe(join(homeDir, 'company-opencode'))
}
)
})