diff --git a/src/main/providers/local-pty-shell-ready.test.ts b/src/main/providers/local-pty-shell-ready.test.ts index 677c2c65946..4f42e866a9b 100644 --- a/src/main/providers/local-pty-shell-ready.test.ts +++ b/src/main/providers/local-pty-shell-ready.test.ts @@ -734,7 +734,19 @@ describePosix('local PTY shell-ready launch config', () => { const zshenv = readFileSync(join(userDataPath, 'shell-ready', 'zsh', '.zshenv'), 'utf8') - expect(zshenv).toContain('_orca_wrapper_zdotdir_self="${ZDOTDIR:-}"') + // Why: derive the wrapper dir from %x (zsh's internal script name), not the + // env-imported $ZDOTDIR — zsh corrupts environment values whose UTF-8 bytes + // fall in its 0x84-0x9D token range (non-ASCII Windows usernames), which + // would fail the self-check and fall back to the unusable baked literal. + expect(zshenv).toContain('_orca_wrapper_zdotdir_self="${${(%):-%x}:h}"') + // Keep $ZDOTDIR only as a fallback when %x expansion yields nothing. The + // final restore below re-validates with -f before trusting the value, so no + // stat is needed here. + expect(zshenv).toContain( + 'if [[ -z "${_orca_wrapper_zdotdir_self:-}" ]]; then\n' + + ' _orca_wrapper_zdotdir_self="${ZDOTDIR:-}"\n' + + 'fi' + ) // 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( @@ -745,7 +757,7 @@ describePosix('local PTY shell-ready launch config', () => { 'fi' ) // Capture must happen before the wrapper unsets ZDOTDIR to source user files. - expect(zshenv.indexOf('_orca_wrapper_zdotdir_self="${ZDOTDIR:-}"')).toBeLessThan( + expect(zshenv.indexOf('_orca_wrapper_zdotdir_self="${${(%):-%x}:h}"')).toBeLessThan( zshenv.indexOf('unset ZDOTDIR') ) }) @@ -892,6 +904,52 @@ path=(/custom/bin $path) } }) + it('loads user .zshrc when the wrapper dir contains a non-ASCII (token-range) path', async () => { + // Why: issue #8003 second trigger — a non-ASCII Windows username (e.g. a + // Korean login) puts UTF-8 bytes in zsh's 0x84-0x9D token range into the + // wrapper path. zsh corrupts the env-imported $ZDOTDIR while processing + // startup files, so deriving the wrapper dir from $ZDOTDIR fails the + // self-check and falls back to the unusable baked literal, leaving the + // user's .zshrc unloaded. Deriving from %x sidesteps the corruption. + writeFileSync(join(testHome, '.zshrc'), 'export USER_ZSHRC_LOADED=yes\n') + + const { getShellReadyLaunchConfig } = await importFreshLocalPtyShellReady() + getShellReadyLaunchConfig('/bin/zsh') + + // Move the generated wrappers under a non-ASCII runtime root so the baked + // literal is unusable and the runtime $ZDOTDIR gets corrupted on import. + const nonAsciiUserData = join(dirname(userDataPath), '홍길동-wsl-view') + renameSync(userDataPath, nonAsciiUserData) + 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(nonAsciiUserData, 'shell-ready', 'zsh') + + for (const args of [['-i'], ['-l', '-i']] as const) { + const result = spawnSync( + 'zsh', + [...args, '-c', 'echo "USER_ZSHRC_LOADED=${USER_ZSHRC_LOADED:-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') + } + } finally { + rmSync(nonAsciiUserData, { 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 d11bdbcfbc0..da520112dca 100644 --- a/src/main/shell-templates.ts +++ b/src/main/shell-templates.ts @@ -13,7 +13,17 @@ export function getZshEnvTemplate(zshDir: string, headerPrefix = ''): string { # 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:-}" +# Derive it from the file being sourced (%x, zsh's internal script name) rather +# than the env-imported $ZDOTDIR: zsh corrupts environment values whose UTF-8 +# bytes fall in its 0x84-0x9D token range (e.g. a non-ASCII Windows username +# such as a Korean login), which would make the self-check below fail and fall +# back to the unusable baked literal, so the user's .zshrc never loads (#8003). +# %x is not subject to that corruption; keep $ZDOTDIR as a fallback for the +# rare shell where %x prompt expansion yields nothing. +_orca_wrapper_zdotdir_self="\${\${(%):-%x}:h}" +if [[ -z "\${_orca_wrapper_zdotdir_self:-}" ]]; then + _orca_wrapper_zdotdir_self="\${ZDOTDIR:-}" +fi while [[ "\${_orca_wrapper_zdotdir_self:-}" == */ ]]; do _orca_wrapper_zdotdir_self="\${_orca_wrapper_zdotdir_self%/}" done