Bug wsl terminal zsh shell ready wrapper exports (#8025)

* Fix WSL zsh ZDOTDIR restore using runtime-sourced path instead of baked

Windows-generated shell-ready wrappers are sourced via /mnt/c on WSL, where
the generation-time path baked into .zshenv doesn't exist. Capture the
runtime ZDOTDIR before it's unset and prefer it (when it still points to a
valid wrapper dir) over the baked literal when restoring ZDOTDIR, so user
.zshrc/.zshenv files load correctly under WSL.

* Fix zsh ZDOTDIR self-restore to guard against unset variables

Add ":-" default expansion when referencing _orca_wrapper_zdotdir_self so
the wrapper doesn't abort under `set -u` when the variable is unset, and
extend the shell-ready test to assert login-shell behavior (IS_LOGIN)
and improve failure diagnostics on zsh spawn.
This commit is contained in:
Jinjing 2026-07-09 22:41:58 -07:00 committed by GitHub
parent 4908a03671
commit 620b475acd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 3 deletions

View File

@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { spawnSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import { join, dirname } from 'node:path'
import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'
import { mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'
import type * as pty from 'node-pty'
import type * as LocalPtyShellReadyModule from './local-pty-shell-ready'
import {
@ -724,6 +724,31 @@ describePosix('local PTY shell-ready launch config', () => {
// Fallback chain: discovered → normalized spawn-env path → HOME
expect(zshenv).toContain('${_orca_discovered_zdotdir:-${_orca_user_zdotdir:-$HOME}}')
})
it('restores wrapper ZDOTDIR from the runtime sourced path, not the baked literal', async () => {
// Why: issue #8003 — WSL sources Windows-generated wrappers via /mnt/c,
// so the generation-time path baked into .zshenv does not exist there.
const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady()
getShellReadyLaunchConfig('/bin/zsh')
const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8')
expect(zshenv).toContain('_orca_wrapper_zdotdir_self="${ZDOTDIR:-}"')
// The runtime path is only trusted when it still holds a wrapper .zshenv;
// otherwise the generation-time literal remains as the fallback.
expect(zshenv).toContain(
'if [[ -n "${_orca_wrapper_zdotdir_self:-}" && -f "${_orca_wrapper_zdotdir_self:-}/.zshenv" ]]; then\n' +
' export ZDOTDIR="${_orca_wrapper_zdotdir_self:-}"\n' +
'else\n' +
` export ZDOTDIR='${join(userDataPath, 'shell-ready', 'zsh')}'\n` +
'fi'
)
// Capture must happen before the wrapper unsets ZDOTDIR to source user files.
expect(zshenv.indexOf('_orca_wrapper_zdotdir_self="${ZDOTDIR:-}"')).toBeLessThan(
zshenv.indexOf('unset ZDOTDIR')
)
})
})
// Why: end-to-end validation that wrapper ZDOTDIR discovery preserves top-level
@ -812,6 +837,59 @@ path=(/custom/bin $path)
expect(output).toContain('PATH_HAS_CUSTOM=/custom/bin')
})
it('loads user .zshrc when wrappers are sourced from a different runtime path (WSL simulation)', async () => {
// Why: issue #8003 — on Windows the wrappers are generated under the
// native userData path but WSL sources them via /mnt/c, where the baked
// generation-time path does not exist. Renaming the userData dir after
// generation reproduces that split: runtime ZDOTDIR resolves, the baked
// literal does not.
writeFileSync(join(testHome, '.zshrc'), 'export USER_ZSHRC_LOADED=yes\n')
const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady()
getShellReadyLaunchConfig('/bin/zsh')
const movedUserData = `${userDataPath}-wsl-view`
renameSync(userDataPath, movedUserData)
try {
const cleanEnv: Record<string, string | undefined> = {
...process.env,
HOME: testHome,
PATH: '/usr/bin:/bin'
}
delete cleanEnv.ZDOTDIR
delete cleanEnv.ORCA_ORIG_ZDOTDIR
delete cleanEnv.ORCA_ATTRIBUTION_SHIM_DIR
delete cleanEnv.USER_ZSHRC_LOADED
cleanEnv.ZDOTDIR = join(movedUserData, 'shell-ready', 'zsh')
// Production WSL launches a login shell (`exec zsh -l`); also cover the
// non-login flow used by local panes so both restore paths stay pinned.
// Login must still load user .zshrc (via wrapper .zshrc after .zprofile)
// and leave final ZDOTDIR at the user home after .zlogin restore.
for (const args of [['-i'], ['-l', '-i']] as const) {
const result = spawnSync(
'zsh',
[
...args,
'-c',
'echo "USER_ZSHRC_LOADED=${USER_ZSHRC_LOADED:-no}" && echo "FINAL_ZDOTDIR=${ZDOTDIR:-unset}" && echo "IS_LOGIN=$([[ -o login ]] && echo yes || echo no)"'
],
{
env: cleanEnv as NodeJS.ProcessEnv,
encoding: 'utf8'
}
)
expect(result.status, `zsh ${args.join(' ')} failed: ${result.stderr}`).toBe(0)
expect(result.stdout).toContain('USER_ZSHRC_LOADED=yes')
expect(result.stdout).toContain(`FINAL_ZDOTDIR=${testHome}`)
expect(result.stdout).toContain(args.includes('-l') ? 'IS_LOGIN=yes' : 'IS_LOGIN=no')
}
} finally {
rmSync(movedUserData, { recursive: true, force: true })
}
})
it('preserves top-level .zshenv path and function side effects', async () => {
// Why: .zshenv is the normal place for always-on zsh env/path setup.
// Dropping those side effects regresses non-Orca zsh startup semantics.

View File

@ -10,6 +10,13 @@ export function getZshEnvTemplate(zshDir: string, headerPrefix = ''): string {
? `Orca ${headerPrefix} zsh shell-ready wrapper`
: 'Orca zsh shell-ready wrapper'
return `# ${header}
# Why: capture the runtime wrapper dir before it is unset below. On WSL this
# file is generated with a Windows path but sourced via /mnt/c, so the baked
# literal is unusable there and ZDOTDIR must be restored from this value.
_orca_wrapper_zdotdir_self="\${ZDOTDIR:-}"
while [[ "\${_orca_wrapper_zdotdir_self:-}" == */ ]]; do
_orca_wrapper_zdotdir_self="\${_orca_wrapper_zdotdir_self%/}"
done
_orca_spawn_orig_zdotdir="\${ORCA_ORIG_ZDOTDIR:-}"
_orca_user_zdotdir="\${_orca_spawn_orig_zdotdir:-$HOME}"
_orca_zshenv_source_dir="\${ORCA_ZSHENV_SOURCE_DIR:-$HOME}"
@ -67,8 +74,14 @@ case "\${ORCA_ORIG_ZDOTDIR}" in
""|*/shell-ready/zsh) export ORCA_ORIG_ZDOTDIR="$HOME" ;;
esac
export ZDOTDIR=${quotePosixSingle(zshDir)}
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir
# Why: use :- after user .zshenv a pathological unset under set -u must not
# abort the wrapper; empty falls through to the baked-literal branch.
if [[ -n "\${_orca_wrapper_zdotdir_self:-}" && -f "\${_orca_wrapper_zdotdir_self:-}/.zshenv" ]]; then
export ZDOTDIR="\${_orca_wrapper_zdotdir_self:-}"
else
export ZDOTDIR=${quotePosixSingle(zshDir)}
fi
unset _orca_spawn_orig_zdotdir _orca_user_zdotdir _orca_zshenv_source_dir _orca_zshenv_path _orca_discovered_zdotdir _orca_wrapper_zdotdir_self
`
}