mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
fix(shell): make every wrapper file self-sufficient and retire the deleted marker vars from tests
- .zprofile/.zshrc/.zlogin each define __orca_resolve_user_config_dir. They called it on line 2 while only .zshenv defined it, so a wrapper dir written by two concurrently installed builds printed three "command not found" and skipped the user's entire zsh config. New live-shell test covers it. - Retarget every remaining ORCA_SHELL_READY_MARKER/ORCA_SHELL_STARTUP_IDENTITY reference onto ORCA_SHELL_FEATURES, or delete it where the key is now dead. - isOrcaMintedHistFile requires a leading '/', so a relative path of the same shape stays the user's. - Drop an unused no-control-regex disable, and register the two real-zsh suites in the dedicated shell-contracts lane.
This commit is contained in:
@@ -336,8 +336,10 @@ jobs:
|
||||
src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.test.ts \
|
||||
src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
src/main/shell-startup-feature-channel.test.ts \
|
||||
src/main/terminal-history-fish-session.node-pty.test.ts \
|
||||
src/main/zsh-scoped-histfile.live-shell.test.ts \
|
||||
src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
|
||||
src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
||||
src/shared/startup-shell-portability.live-shell.test.ts \
|
||||
src/shared/posix-command-path-lookup.test.ts
|
||||
@@ -378,8 +380,10 @@ jobs:
|
||||
--exclude=src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.test.ts \
|
||||
--exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
--exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
--exclude=src/main/shell-startup-feature-channel.test.ts \
|
||||
--exclude=src/main/terminal-history-fish-session.node-pty.test.ts \
|
||||
--exclude=src/main/zsh-scoped-histfile.live-shell.test.ts \
|
||||
--exclude=src/main/zsh-wrapper-version-mismatch.live-shell.test.ts \
|
||||
--exclude=src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
||||
--exclude=src/shared/startup-shell-portability.live-shell.test.ts \
|
||||
--exclude=src/shared/posix-command-path-lookup.test.ts \
|
||||
|
||||
@@ -15,7 +15,9 @@ const shellContractFiles = [
|
||||
'src/main/providers/local-pty-shell-ready-zsh-zdotdir-discovery.test.ts',
|
||||
'src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.test.ts',
|
||||
'src/main/providers/__tests__/shell-ready-framework-example.test.ts',
|
||||
'src/main/shell-startup-feature-channel.test.ts',
|
||||
'src/main/zsh-scoped-histfile.live-shell.test.ts',
|
||||
'src/main/zsh-wrapper-version-mismatch.live-shell.test.ts',
|
||||
'src/shared/posix-command-path-lookup.test.ts'
|
||||
]
|
||||
const patchedNodePtyContractFiles = [
|
||||
|
||||
@@ -29,7 +29,7 @@ function runInteractiveBash(
|
||||
{
|
||||
input,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, HOME: tempHome, ORCA_SHELL_READY_MARKER: '1', TERM: 'xterm' },
|
||||
env: { ...process.env, HOME: tempHome, ORCA_SHELL_FEATURES: 'ready', TERM: 'xterm' },
|
||||
timeout: 5000
|
||||
}
|
||||
)
|
||||
|
||||
@@ -233,14 +233,14 @@ describe('createPtySubprocess', () => {
|
||||
resolveUnixShellPathMock.mockReturnValue('/bin/sh')
|
||||
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
const previousShell = process.env.SHELL
|
||||
const previousMarker = process.env.ORCA_SHELL_READY_MARKER
|
||||
const previousFeatures = process.env.ORCA_SHELL_FEATURES
|
||||
const previousZdotdir = process.env.ZDOTDIR
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
|
||||
delete process.env.SHELL
|
||||
// Why: the test runner itself can execute inside an Orca-wrapped shell
|
||||
// whose exported wrapper vars would leak through the process.env spread.
|
||||
delete process.env.ORCA_SHELL_READY_MARKER
|
||||
delete process.env.ORCA_SHELL_FEATURES
|
||||
delete process.env.ZDOTDIR
|
||||
|
||||
try {
|
||||
@@ -257,9 +257,9 @@ describe('createPtySubprocess', () => {
|
||||
expect(shellPath).toBe('/bin/sh')
|
||||
expect(shellArgs).toEqual(['-l'])
|
||||
// A launch config derived from the missing preferred zsh would inject
|
||||
// ZDOTDIR and ORCA_SHELL_READY_MARKER; /bin/sh must spawn without them.
|
||||
// ZDOTDIR and ORCA_SHELL_FEATURES; /bin/sh must spawn without them.
|
||||
expect(spawnOptions.env.ZDOTDIR).toBeUndefined()
|
||||
expect(spawnOptions.env.ORCA_SHELL_READY_MARKER).toBeUndefined()
|
||||
expect(spawnOptions.env.ORCA_SHELL_FEATURES).toBeUndefined()
|
||||
expect(spawnOptions.env.SHELL).toBe('/bin/sh')
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
@@ -271,10 +271,10 @@ describe('createPtySubprocess', () => {
|
||||
} else {
|
||||
process.env.SHELL = previousShell
|
||||
}
|
||||
if (previousMarker === undefined) {
|
||||
delete process.env.ORCA_SHELL_READY_MARKER
|
||||
if (previousFeatures === undefined) {
|
||||
delete process.env.ORCA_SHELL_FEATURES
|
||||
} else {
|
||||
process.env.ORCA_SHELL_READY_MARKER = previousMarker
|
||||
process.env.ORCA_SHELL_FEATURES = previousFeatures
|
||||
}
|
||||
if (previousZdotdir === undefined) {
|
||||
delete process.env.ZDOTDIR
|
||||
|
||||
@@ -412,7 +412,8 @@ describe('registerPtyHandlers', () => {
|
||||
cwd: '/tmp',
|
||||
env: expect.objectContaining({
|
||||
ORCA_OPENCODE_CONFIG_DIR: '/tmp/orca-opencode-config',
|
||||
ORCA_SHELL_READY_MARKER: '0',
|
||||
// No `ready`: the fallback shell carries an overlay, not a startup command.
|
||||
ORCA_SHELL_FEATURES: 'overlay,history,markers',
|
||||
ZDOTDIR: '/tmp/orca-user-data/shell-ready/zsh'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ EOF
|
||||
- Lines before the marker → setup commands
|
||||
- Lines after the marker → run command to test
|
||||
|
||||
3. **Gets Orca's wrapper config** by calling `getShellReadyLaunchConfig()`
|
||||
3. **Gets Orca's wrapper config** by calling `getShellLaunchConfig()` with the features `selectShellStartupFeatures()` picks for a startup-command launch
|
||||
|
||||
4. **Executes setup** (if present) with bash in temp HOME, using wrapper env
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ function runInteractiveBashRcfile(
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: tempDir,
|
||||
ORCA_SHELL_READY_MARKER: '1',
|
||||
ORCA_SHELL_FEATURES: 'ready',
|
||||
TERM: process.env.TERM || 'xterm'
|
||||
},
|
||||
timeout: 5000
|
||||
|
||||
@@ -72,8 +72,8 @@ function launchCodexThroughRcfile(fixture: Fixture, preflightValue: string): voi
|
||||
PATH: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'].join(delimiter),
|
||||
TERM: 'dumb',
|
||||
SHELL: '/bin/bash',
|
||||
ORCA_SHELL_READY_MARKER: '0',
|
||||
ORCA_SHELL_STARTUP_IDENTITY: '0',
|
||||
// Why no ORCA_SHELL_FEATURES: absent means no features, so the rcfile
|
||||
// emits neither the identity nor the readiness marker into stdout.
|
||||
ORCA_CODEX_HOME: join(fixture.root, 'codex-home'),
|
||||
ORCA_CODEX_LAUNCH_PREFLIGHT: preflightValue
|
||||
}
|
||||
|
||||
@@ -371,8 +371,7 @@ describePosix('history-only pane in a real zsh', () => {
|
||||
env: { PATH: '/usr/bin:/bin', HOME: home }
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(wrapped).not.toMatch(/]133;/)
|
||||
expect(wrapped).not.toContain('\x1b]133;')
|
||||
expect(wrapped).toContain('PRECMD=\n')
|
||||
expect(wrapped).toContain('LINEINIT=none')
|
||||
// Startup files run in the same order with the same hooks, so the pane is
|
||||
|
||||
@@ -45,6 +45,12 @@ export const SHELL_STARTUP_IDENTITY_MARKER_BLOCK = `__orca_has_feature identity
|
||||
* its own. A stamped marker file (or Orca's own wrapper path shape, for
|
||||
* wrappers written by older builds) is that proof. Guessing at other
|
||||
* terminals' wrapper dirs by name never can be, so this never tries.
|
||||
*
|
||||
* Why every generated file redefines it instead of relying on .zshenv: one
|
||||
* wrapper dir can be rewritten by two concurrently installed builds, so a shell
|
||||
* can read one build's .zshenv and another's .zshrc. A .zshrc that called a
|
||||
* function only the newer .zshenv defines printed `command not found` AND left
|
||||
* $REPLY empty, which skipped sourcing the user's own startup file entirely.
|
||||
*/
|
||||
export const ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK = `__orca_resolve_user_config_dir() {
|
||||
REPLY="\${1:-}"
|
||||
@@ -52,8 +58,15 @@ export const ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK = `__orca_resolve_user_config_di
|
||||
if [[ -z "$REPLY" || -f "$REPLY/${ZSH_WRAPPER_DIR_MARKER_FILE}" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
# Why stricter for an inherited value: Orca can be launched from a terminal that
|
||||
}`
|
||||
|
||||
/**
|
||||
* The stricter resolver .zshenv uses for a ZDOTDIR this shell INHERITED.
|
||||
*
|
||||
* Why only .zshenv needs it: nothing after .zshenv re-reads an inherited value —
|
||||
* the later files resolve ORCA_ORIG_ZDOTDIR, which .zshenv already vetted.
|
||||
*/
|
||||
export const ZSH_INHERITED_CONFIG_DIR_RESOLVER_BLOCK = `# Why stricter for an inherited value: Orca can be launched from a terminal that
|
||||
# already pointed ZDOTDIR at its own wrapper dir, and a directory holding no zsh
|
||||
# startup file at all is not the user's config root whoever wrote it.
|
||||
__orca_resolve_inherited_config_dir() {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca daemon zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca daemon zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca daemon zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca zsh shell-ready wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_ORIG_ZDOTDIR:-$HOME}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ "$_orca_home" != "$ZDOTDIR" && -o interactive && -f "$_orca_home/.zshrc" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca relay zsh overlay wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -o interactive && -f "$_orca_home/.zlogin" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca relay zsh overlay wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -f "$_orca_home/.zprofile" ]]; then
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Orca relay zsh overlay wrapper
|
||||
__orca_resolve_user_config_dir() {
|
||||
REPLY="${1:-}"
|
||||
while [[ "$REPLY" == */ ]]; do REPLY="${REPLY%/}"; done
|
||||
if [[ -z "$REPLY" || -f "$REPLY/.orca-shell-wrapper" || "$REPLY" == */shell-ready/zsh ]]; then
|
||||
REPLY="$HOME"
|
||||
fi
|
||||
}
|
||||
__orca_resolve_user_config_dir "${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"
|
||||
_orca_home="$REPLY"
|
||||
if [[ -o interactive && -f "$_orca_home/.zshrc" ]]; then
|
||||
|
||||
@@ -402,13 +402,21 @@ describe('terminal-history', () => {
|
||||
expect(env.HISTFILE).not.toContain(OTHER_WORKTREE_HASH)
|
||||
})
|
||||
|
||||
it('preserves a HISTFILE the user set themselves', () => {
|
||||
it.each([
|
||||
['an ordinary path', ['', 'home', 'me', '.zsh_history'].join(sep)],
|
||||
// Orca only ever mints absolute paths, so the same shape relative to the
|
||||
// user's cwd is theirs.
|
||||
[
|
||||
'a relative path of Orca’s shape',
|
||||
['terminal-history', OTHER_WORKTREE_HASH, 'zsh_history'].join(sep)
|
||||
]
|
||||
])('preserves %s the user set as HISTFILE', (_kind, histFile) => {
|
||||
// Only a path Orca minted is droppable; everything else is the user's.
|
||||
const env: Record<string, string> = { HISTFILE: ['', 'home', 'me', '.zsh_history'].join(sep) }
|
||||
const env: Record<string, string> = { HISTFILE: histFile }
|
||||
|
||||
const result = injectHistoryEnv(env, 'repo-1::/path/wt', '/bin/zsh', '/path/wt')
|
||||
|
||||
expect(env.HISTFILE).toBe(['', 'home', 'me', '.zsh_history'].join(sep))
|
||||
expect(env.HISTFILE).toBe(histFile)
|
||||
expect(result.histFile).toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -15,14 +15,27 @@
|
||||
* the desktop (Electron userData), the daemon subprocess (no Electron), and the
|
||||
* relay on a remote host. Desktop and relay drop each other's shapes on purpose
|
||||
* — neither owns the other's worktree ids.
|
||||
*
|
||||
* Why the shape is enough without an Orca-specific token: two of the three
|
||||
* shapes carry one already (`.orca-remote`, `terminal-history-wsl`), and the
|
||||
* third needs an absolute path whose LAST TWO segments are a 16-char lowercase
|
||||
* hex directory directly under `terminal-history`, holding a file named exactly
|
||||
* `zsh_history`/`bash_history`. That is a machine-minted layout, not one a
|
||||
* person types. The blast radius if it were ever hit is also bounded: the value
|
||||
* is dropped from ONE spawn's env, so the pane gets Orca's own worktree history
|
||||
* (isolation on) or the shell's default (isolation off). Nothing on disk is
|
||||
* read, written, moved, or deleted.
|
||||
*/
|
||||
|
||||
/** 16 hex chars: `hashWorktreeId`. */
|
||||
const WORKTREE_HASH = '[0-9a-f]{16}'
|
||||
const HISTORY_FILE = '(?:zsh|bash)_history'
|
||||
|
||||
// Why a leading `/` rather than `(?:^|/)`: every minted value is absolute (or a
|
||||
// Windows path normalized to forward slashes), so a relative path of the same
|
||||
// shape is the user's, not Orca's.
|
||||
const ORCA_MINTED_HISTFILE = new RegExp(
|
||||
'(?:^|/)(?:' +
|
||||
'/(?:' +
|
||||
// Desktop: <userData>/terminal-history/<hash>/<file>
|
||||
`terminal-history/${WORKTREE_HASH}/${HISTORY_FILE}` +
|
||||
'|' +
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
* disagree on is a field on ZshStartupWrapperSpec, so the disagreements are
|
||||
* visible in one place instead of spread across three template literals.
|
||||
*
|
||||
* Every generated file redefines the helpers it calls, so the epilogue is the
|
||||
* only thing one file needs another to have defined — see EPILOGUE_CALL.
|
||||
*
|
||||
* All order-sensitive Orca work lives in ONE `__orca_shell_epilogue` defined in
|
||||
* .zshenv and invoked exactly once — from .zshrc for a non-login shell, from
|
||||
* .zlogin for a login shell. Each feature inside it is an independent guard on
|
||||
@@ -26,6 +29,7 @@ import {
|
||||
SHELL_STARTUP_IDENTITY_MARKER_BLOCK,
|
||||
ZSH_FEATURE_CHANNEL_BLOCK,
|
||||
ZSH_HISTFILE_RESTORE_BLOCK,
|
||||
ZSH_INHERITED_CONFIG_DIR_RESOLVER_BLOCK,
|
||||
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK
|
||||
} from './shell-templates'
|
||||
|
||||
@@ -180,6 +184,7 @@ function buildZshenv(spec: ZshStartupWrapperSpec): string {
|
||||
ZSH_FEATURE_CHANNEL_BLOCK,
|
||||
SHELL_STARTUP_IDENTITY_MARKER_BLOCK,
|
||||
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK,
|
||||
ZSH_INHERITED_CONFIG_DIR_RESOLVER_BLOCK,
|
||||
spec.zshenvStrategy === 'discover-user-zdotdir'
|
||||
? getZshEnvDiscoveryBody(spec.zshDir)
|
||||
: getZshOverlayEnvBody(spec.zshDir),
|
||||
@@ -188,12 +193,13 @@ function buildZshenv(spec: ZshStartupWrapperSpec): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Why guarded: the generated files are inter-dependent (this call, the function
|
||||
* in .zshenv), and one wrapper dir can be shared by two concurrently installed
|
||||
* Orca builds. Pairing this file with an older .zshenv then loses the features
|
||||
* either way; degrade silently instead of printing "command not found" into the
|
||||
* user's pane. Braced subscript because sh/ksh emulation (KSH_ARRAYS) rejects
|
||||
* the unbraced `$+functions[...]` form outright.
|
||||
* Why guarded: one wrapper dir can be shared by two concurrently installed Orca
|
||||
* builds, so a shell can read one build's .zshenv and another's .zshrc/.zlogin.
|
||||
* The epilogue is the only cross-file dependency left — every other function
|
||||
* these files call is redefined at the top of each of them — so an older .zshenv
|
||||
* costs this shell the epilogue's features and nothing else, without printing
|
||||
* "command not found" into the user's pane. Braced subscript because sh/ksh
|
||||
* emulation (KSH_ARRAYS) rejects the unbraced `$+functions[...]` form outright.
|
||||
*/
|
||||
const EPILOGUE_CALL = '(( ${+functions[__orca_shell_epilogue]} )) && __orca_shell_epilogue'
|
||||
|
||||
@@ -217,6 +223,7 @@ export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshSta
|
||||
zshenv: buildZshenv(spec),
|
||||
zprofile: `${joinBlocks([
|
||||
`# ${spec.headerLabel}`,
|
||||
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK,
|
||||
getZshStartupFileSourceBlock({
|
||||
fileName: '.zprofile',
|
||||
homeExpression: spec.homeExpression
|
||||
@@ -224,6 +231,7 @@ export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshSta
|
||||
])}\n`,
|
||||
zshrc: `${joinBlocks([
|
||||
`# ${spec.headerLabel}`,
|
||||
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK,
|
||||
getZshStartupFileSourceBlock({
|
||||
fileName: '.zshrc',
|
||||
homeExpression: spec.homeExpression,
|
||||
@@ -234,6 +242,7 @@ export function buildZshStartupWrapperFiles(spec: ZshStartupWrapperSpec): ZshSta
|
||||
])}\n`,
|
||||
zlogin: `${joinBlocks([
|
||||
`# ${spec.headerLabel}`,
|
||||
ZSH_USER_CONFIG_DIR_RESOLVER_BLOCK,
|
||||
getZshStartupFileSourceBlock({
|
||||
fileName: '.zlogin',
|
||||
homeExpression: spec.homeExpression,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Real-zsh proof that a wrapper dir written by two different Orca builds still
|
||||
* loads the user's own zsh config.
|
||||
*
|
||||
* One wrapper dir (`<userData>/shell-ready/zsh`, or `~/.orca-relay/shell-ready/
|
||||
* zsh` for a remote host) is shared by every concurrently installed build, and
|
||||
* each rewrites it on spawn. A shell can therefore read one build's `.zshenv`
|
||||
* and another's `.zprofile`/`.zshrc`/`.zlogin`. Only a real zsh shows the cost:
|
||||
* a helper the newer files call but the older `.zshenv` never defined both
|
||||
* prints `command not found` into the pane AND leaves `$REPLY` empty, which
|
||||
* silently skips sourcing the user's own startup files.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ensureShellReadyWrappersAt } from './providers/local-pty-shell-ready-wrapper-generation'
|
||||
|
||||
const hasZsh = process.platform !== 'win32' && spawnSync('zsh', ['--version']).status === 0
|
||||
const ZSH_PATH = hasZsh
|
||||
? (spawnSync('sh', ['-c', 'command -v zsh'], { encoding: 'utf8' }).stdout || '').trim()
|
||||
: ''
|
||||
const itWithZsh = hasZsh ? it : it.skip
|
||||
|
||||
/**
|
||||
* A `.zshenv` from a build that predates everything the other three files now
|
||||
* call: it exports the wrapper ZDOTDIR and nothing else.
|
||||
*/
|
||||
function olderBuildZshenv(zshDir: string): string {
|
||||
return `# Orca zsh shell-ready wrapper
|
||||
export ORCA_ORIG_ZDOTDIR="$HOME"
|
||||
export ZDOTDIR=${JSON.stringify(zshDir)}
|
||||
`
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('zsh wrapper dir written by mixed builds', () => {
|
||||
itWithZsh('still sources the user config when the .zshenv is from an older build', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-wrapper-mismatch-'))
|
||||
const home = mkdtempSync(join(tmpdir(), 'orca-wrapper-mismatch-home-'))
|
||||
try {
|
||||
expect(ensureShellReadyWrappersAt(root)).toBe(true)
|
||||
const zshDir = join(root, 'zsh')
|
||||
writeFileSync(join(zshDir, '.zshenv'), olderBuildZshenv(zshDir))
|
||||
for (const file of ['.zprofile', '.zshrc', '.zlogin']) {
|
||||
writeFileSync(join(home, file), `echo "RAN ${file}"\n`)
|
||||
}
|
||||
|
||||
const result = spawnSync(ZSH_PATH, ['-l', '-i', '-c', 'exit 0'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 20_000,
|
||||
env: { PATH: '/usr/bin:/bin', HOME: home, ZDOTDIR: zshDir }
|
||||
})
|
||||
// Why both streams: zsh prints `command not found` on stderr and the
|
||||
// fixture files echo on stdout, and this asserts about each.
|
||||
const output = `${result.stdout}${result.stderr}`
|
||||
|
||||
expect(output).not.toContain('command not found')
|
||||
expect(output).toContain('RAN .zprofile')
|
||||
expect(output).toContain('RAN .zshrc')
|
||||
expect(output).toContain('RAN .zlogin')
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user