diff --git a/src/main/pty/shell-startup/codex-shell-launch-preflight.test.ts b/src/main/pty/shell-startup/codex-shell-launch-preflight.test.ts index 3a70d313fe3..092fd5a7bc5 100644 --- a/src/main/pty/shell-startup/codex-shell-launch-preflight.test.ts +++ b/src/main/pty/shell-startup/codex-shell-launch-preflight.test.ts @@ -231,6 +231,29 @@ describe.skipIf(process.platform === 'win32')('Codex shell launch preflight', () ) } + for (const shell of ['/bin/bash', '/bin/zsh']) { + it.skipIf(!existsSync(shell))( + `keeps the user’s own codex function in ${shell} when the cwd has a codex entry`, + () => { + const root = mkdtempSync(join(tmpdir(), 'orca-codex-user-fn-')) + roots.push(root) + mkdirSync(join(root, 'codex')) + + const output = execFileSync( + shell, + [ + shell.endsWith('/zsh') ? '-f' : '--noprofile', + '-c', + `codex() { printf 'user function'; }\n${getPosixCodexShellLaunchPreflight()}\ncodex` + ], + { encoding: 'utf-8', cwd: root, env: { ...process.env, PATH: '/usr/bin:/bin' } } + ) + + expect(output).toBe('user function') + } + ) + } + // Fail explicitly when CI requires fish; skipped regression cases cannot establish coverage. // #16893: absent Codex made unquoted (type -t codex) vanish and test complain; exercise real -l -C startup. diff --git a/src/shared/claude-shell-function.test.ts b/src/shared/claude-shell-function.test.ts index 828722c9611..a0bd376d187 100644 --- a/src/shared/claude-shell-function.test.ts +++ b/src/shared/claude-shell-function.test.ts @@ -36,7 +36,12 @@ function fixture() { ) chmodSync(fake, 0o700) const pointer = join(root, 'selected') - const run = (shell: string, env: string[] = [], fakeKind: 'summary' | 'printenv' = 'summary') => { + const run = ( + shell: string, + env: string[] = [], + fakeKind: 'summary' | 'printenv' = 'summary', + userSetup = '' + ) => { if (fakeKind === 'printenv') { writeFileSync(fake, '#!/bin/sh\nenv\n') } @@ -58,7 +63,7 @@ function fixture() { shell, ...(fish ? ['--no-config'] : shell.endsWith('zsh') ? ['-f'] : ['--norc', '--noprofile']), '-c', - `${guard}\n${fn}\nclaude` + `${guard}\n${userSetup}\n${fn}\nclaude` ], { encoding: 'utf8', cwd: root } ) @@ -150,6 +155,17 @@ describe.each(SHELLS)('the claude function in %s', (shell) => { expect(existsSync(join(f.a, '..', 'not-a-folder'))).toBe(false) }) + it.skipIf(!POSIX_SHELLS.includes(shell))( + 'keeps the user’s own claude function when the cwd has a claude entry', + () => { + const f = fixture() + writeFileSync(f.pointer, f.a) + mkdirSync(join(f.a, '..', 'claude')) + const userFn = 'claude() { echo user function; }' + expect(f.run(shell, [], 'summary', userFn).stdout).toBe('user function\n') + } + ) + it('follows the selection over the user’s own CLAUDE_CONFIG_DIR, inherited or exported by an rc', () => { const f = fixture() // Why the slash: Claude keys its login by the exact string, so System default keeps the spelling. diff --git a/src/shared/claude-shell-function.ts b/src/shared/claude-shell-function.ts index d83ae79f918..5fc36f348bc 100644 --- a/src/shared/claude-shell-function.ts +++ b/src/shared/claude-shell-function.ts @@ -1,7 +1,8 @@ /** Re-read account selection on every launch in Orca-routed panes with a real claude binary. Missing/empty pointer restores the user’s config; another pane override wins. Create account homes without a login silently and preserve Anthropic auth. Refuse nonabsolute pointers rather than run under the wrong account. */ export function getPosixClaudeShellFunction(): string { return `__orca_claude_binary="$(unalias claude 2>/dev/null || :; command -v claude 2>/dev/null || :)" -if [[ -n "\${ORCA_CLAUDE_PROFILE_POINTER:-}" && -n "\${__orca_claude_binary:-}" && -x "\${__orca_claude_binary}" ]]; then +# Why typeset -f: a user's own claude function wins; command -v prints its bare name, which -x tests against the cwd. +if ! typeset -f claude >/dev/null 2>&1 && [[ -n "\${ORCA_CLAUDE_PROFILE_POINTER:-}" && -n "\${__orca_claude_binary:-}" && -x "\${__orca_claude_binary}" ]]; then function claude { local __orca_claude_home __orca_claude_pointer="\${ORCA_CLAUDE_PROFILE_POINTER:-}" # Why: a WSL pane's pointer is relative to the guest home, which the host cannot know at spawn. diff --git a/src/shared/codex-shell-function.ts b/src/shared/codex-shell-function.ts index c217b2d2109..58bd6bfd766 100644 --- a/src/shared/codex-shell-function.ts +++ b/src/shared/codex-shell-function.ts @@ -34,7 +34,8 @@ export function getPosixCodexShellLaunchPreflight(): string { # Why || : twice — zsh alone aborts inside the substitution, but every shell's # assignment adopts its exit status, so an absent codex trips set -e in bash too. __orca_codex_binary="$(unalias codex 2>/dev/null || :; command -v codex 2>/dev/null || :)" -if [[ -n "\${__orca_codex_binary:-}" && -x "\${__orca_codex_binary}" ]]; then +# Why typeset -f: a user's own codex function wins; command -v prints its bare name, which -x tests against the cwd. +if ! typeset -f codex >/dev/null 2>&1 && [[ -n "\${__orca_codex_binary:-}" && -x "\${__orca_codex_binary}" ]]; then # Why the function reserved word: it suppresses alias expansion of the name, # which otherwise rewrites this header at parse time and aborts the whole file. function codex {