diff --git a/src/main/providers/local-pty-shell-ready.test.ts b/src/main/providers/local-pty-shell-ready.test.ts index d0a6ba137..e72fc25cb 100644 --- a/src/main/providers/local-pty-shell-ready.test.ts +++ b/src/main/providers/local-pty-shell-ready.test.ts @@ -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 = { + ...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. diff --git a/src/main/shell-templates.ts b/src/main/shell-templates.ts index dccd5e2c8..d11bdbcfb 100644 --- a/src/main/shell-templates.ts +++ b/src/main/shell-templates.ts @@ -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 ` }