fix(terminal): preserve Bash exec edge cases

This commit is contained in:
Neil
2026-08-12 22:00:28 -07:00
parent 82302e47f9
commit 16e437afc8
3 changed files with 129 additions and 16 deletions
+4 -3
View File
@@ -181,6 +181,7 @@
"assertions": [
"local zsh and bash wrappers survive Fig-shaped exec replacements",
"bash prompt rewrites are re-pinned before exec and temporary hooks are removed after normal startup",
"bash nounset, redirection-only exec, and failed-exec prompt-array paths preserve user shell state",
"zsh exec targets and redirections expand against the user ZDOTDIR before wrapper re-entry",
"zsh bare exec redirections and user exec functions retain native behavior"
]
@@ -199,8 +200,8 @@
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts src/main/providers/local-pty-shell-ready.test.ts src/relay/pty-shell-launch.test.ts",
"result": "passed",
"durationSeconds": 4.65,
"summary": "All 107 focused local, daemon, and relay assertions passed, including real zsh/bash exec restoration, command modifiers, ZDOTDIR expansion ordering, and DEBUG trap invocation preservation when the user enables functrace during startup."
"durationSeconds": 6.19,
"summary": "All 110 focused local, daemon, and relay assertions passed, including real zsh/bash exec restoration, command modifiers, ZDOTDIR expansion ordering, nounset and prompt-array cleanup, redirection-only exec behavior, and DEBUG trap invocation preservation when the user enables functrace during startup."
}
],
"runtimeBudget": {
@@ -213,7 +214,7 @@
},
"redGreenEvidence": {
"status": "complete",
"evidence": "PR head 05c0ac91dd1 failed the exact exec \"$ZDOTDIR/zsh-replacement\" oracle with status 127 after expanding the target inside Orca's wrapper directory. The just-in-time shim candidate passed that oracle plus all 107 focused assertions. The pre-fix Bash guard reported USER_DEBUG_NESTED=0 after a profile enabled functrace; the reviewed guard reports USER_DEBUG_NESTED=1 while keeping the ordinary non-functrace trap unmultiplied. The existing #13767 red/green evidence independently proves the original replacement-shell readiness failure."
"evidence": "PR head 05c0ac91dd1 failed the exact exec \"$ZDOTDIR/zsh-replacement\" oracle with status 127 after expanding the target inside Orca's wrapper directory. The just-in-time shim candidate passed that oracle. The pre-fix Bash guard reported USER_DEBUG_NESTED=0 after a profile enabled functrace; the reviewed guard reports USER_DEBUG_NESTED=1 while keeping the ordinary non-functrace trap unmultiplied. Exact head 205e63e4338 armed prompt hooks for redirection-only exec, failed with an unbound PROMPT_COMMAND under nounset, and ran a trailing prompt-array element five times; the reviewed candidate keeps the bridge unarmed, restores the replacement hooks, and runs each array element once. All 110 focused assertions pass. The existing #13767 red/green evidence independently proves the original replacement-shell readiness failure."
},
"performanceBudget": {
"required": true,
+57 -12
View File
@@ -63,6 +63,7 @@ __orca_install_exec_prompt_hooks() {
unset PROMPT_COMMAND
PROMPT_COMMAND="$_orca_exec_joined"
fi
PROMPT_COMMAND="\${PROMPT_COMMAND:-}"
PROMPT_COMMAND="\${PROMPT_COMMAND//__orca_exec_osc133_precmd;/}"
PROMPT_COMMAND="\${PROMPT_COMMAND//;__orca_exec_osc133_precmd/}"
PROMPT_COMMAND="\${PROMPT_COMMAND//__orca_exec_osc133_prompt_done;/}"
@@ -90,7 +91,10 @@ __orca_startup_command_is_exec() {
local _orca_exec_command="$1" _orca_exec_char _orca_exec_previous
local _orca_exec_word="" _orca_exec_quote="" _orca_exec_name=""
local _orca_exec_index=0 _orca_exec_nested_parens=0 _orca_exec_nested_braces=0
local _orca_exec_modifier=0 _orca_exec_escaped=0
local _orca_exec_modifier=0 _orca_exec_escaped=0 _orca_exec_found=0
local _orca_exec_has_redirection=0 _orca_exec_skip_option_value=0
local _orca_exec_skip_redirection_target=0 _orca_exec_options_done=0
local _orca_exec_redirection_prefix=""
while (( _orca_exec_index <= \${#_orca_exec_command} )); do
if (( _orca_exec_index == \${#_orca_exec_command} )); then
_orca_exec_char=" "
@@ -126,23 +130,62 @@ __orca_startup_command_is_exec() {
elif [[ "$_orca_exec_char" == "{" && "$_orca_exec_previous" == "$" ]]; then
_orca_exec_nested_braces=1
_orca_exec_word="\${_orca_exec_word}$_orca_exec_char"
elif [[ "$_orca_exec_char" == "<" || "$_orca_exec_char" == ">" ]]; then
if (( !_orca_exec_has_redirection )); then
_orca_exec_redirection_prefix="$_orca_exec_word"
_orca_exec_has_redirection=1
fi
_orca_exec_word="\${_orca_exec_word}$_orca_exec_char"
elif [[ "$_orca_exec_char" == [[:space:]] ]]; then
if [[ -n "$_orca_exec_word" ]]; then
if [[ "$_orca_exec_word" == "exec" ]]; then
return 0
if (( !_orca_exec_found && _orca_exec_has_redirection )) &&
[[ "$_orca_exec_redirection_prefix" == "exec" ]]; then
_orca_exec_found=1
_orca_exec_redirection_prefix=""
fi
_orca_exec_name="\${_orca_exec_word%%=*}"
if (( !_orca_exec_modifier )) && [[ "$_orca_exec_word" == *=* &&
"$_orca_exec_name" == [a-zA-Z_]* && "$_orca_exec_name" != *[!a-zA-Z0-9_]* ]]; then
:
elif (( !_orca_exec_modifier )) && [[ "$_orca_exec_word" == "builtin" || "$_orca_exec_word" == "command" ]]; then
_orca_exec_modifier=1
elif (( _orca_exec_modifier )) && [[ "$_orca_exec_word" == -* ]]; then
:
if (( _orca_exec_found )); then
if (( _orca_exec_skip_redirection_target )); then
_orca_exec_skip_redirection_target=0
elif (( _orca_exec_has_redirection )); then
if [[ -n "$_orca_exec_redirection_prefix" &&
"$_orca_exec_redirection_prefix" == *[!0-9]* &&
! "$_orca_exec_redirection_prefix" =~ ^[{][a-zA-Z_][a-zA-Z0-9_]*[}]$ ]]; then
return 0
fi
case "$_orca_exec_word" in
*">"|*"<"|*">&"|*"<&"|*"<<-"|*">|") _orca_exec_skip_redirection_target=1 ;;
esac
elif (( _orca_exec_skip_option_value )); then
_orca_exec_skip_option_value=0
elif (( _orca_exec_options_done )); then
return 0
else
case "$_orca_exec_word" in
-a) _orca_exec_skip_option_value=1 ;;
-c|-l|-cl|-lc) ;;
--) _orca_exec_options_done=1 ;;
-*) return 1 ;;
*) return 0 ;;
esac
fi
elif [[ "$_orca_exec_word" == "exec" ]]; then
_orca_exec_found=1
else
return 1
_orca_exec_name="\${_orca_exec_word%%=*}"
if (( !_orca_exec_modifier )) && [[ "$_orca_exec_word" == *=* &&
"$_orca_exec_name" == [a-zA-Z_]* && "$_orca_exec_name" != *[!a-zA-Z0-9_]* ]]; then
:
elif (( !_orca_exec_modifier )) && [[ "$_orca_exec_word" == "builtin" || "$_orca_exec_word" == "command" ]]; then
_orca_exec_modifier=1
elif (( _orca_exec_modifier )) && [[ "$_orca_exec_word" == -* ]]; then
:
else
return 1
fi
fi
_orca_exec_word=""
_orca_exec_has_redirection=0
_orca_exec_redirection_prefix=""
fi
else
_orca_exec_word="\${_orca_exec_word}$_orca_exec_char"
@@ -203,8 +246,10 @@ __orca_remove_exec_prompt_hooks() {
[[ -n "$_orca_exec_prompt_part" ]] || continue
_orca_exec_joined="\${_orca_exec_joined}\${_orca_exec_joined:+;}$_orca_exec_prompt_part"
done
unset PROMPT_COMMAND
PROMPT_COMMAND="$_orca_exec_joined"
fi
PROMPT_COMMAND="\${PROMPT_COMMAND:-}"
PROMPT_COMMAND="\${PROMPT_COMMAND//__orca_exec_osc133_precmd;/}"
PROMPT_COMMAND="\${PROMPT_COMMAND//;__orca_exec_osc133_precmd/}"
PROMPT_COMMAND="\${PROMPT_COMMAND//__orca_exec_osc133_prompt_done;/}"
@@ -606,7 +606,7 @@ describePosix('local PTY shell-ready launch config', () => {
'if [[ -z "${ORCA_EXEC_REPRO_DONE:-}" ]]; then',
' export ORCA_EXEC_REPRO_DONE=1',
' PROMPT_COMMAND=\'printf "USER_EXEC_PROMPT\\n"\'',
' Q_START_TEXT="local seed" exec -a figterm-test bash --noprofile --norc -l -i',
' Q_START_TEXT="local seed" exec -a 3> /dev/null figterm-test bash --noprofile --norc -l -i',
'fi',
''
].join('\n')
@@ -641,6 +641,73 @@ describePosix('local PTY shell-ready launch config', () => {
expect(output).toContain('CHILD_TEMP_PC=unset')
})
itWithBash('does not arm bash exec hooks for redirection-only commands', async () => {
const { getBashShellReadyRcfileContent } = await importFreshLocalPtyShellReady()
writeFileSync(
join(userDataPath, '.bash_profile'),
[
'unset PROMPT_COMMAND',
'exec 3>"$HOME/startup-redirection"',
'printf "REDIRECTION_PC=%s:ARMED=%s\\n" "${PROMPT_COMMAND+set}" "$_orca_exec_prompt_hooks_installed"',
'exec 3>&-',
''
].join('\n')
)
const output = runInteractiveBashRcfile(getBashShellReadyRcfileContent(), userDataPath)
expect(output).toContain('REDIRECTION_PC=:ARMED=0')
})
itWithBash('survives nounset before an exec replacement without PROMPT_COMMAND', async () => {
const { getBashShellReadyRcfileContent } = await importFreshLocalPtyShellReady()
writeFileSync(
join(userDataPath, '.bash_profile'),
[
'if [[ -z "${ORCA_EXEC_REPRO_DONE:-}" ]]; then',
' export ORCA_EXEC_REPRO_DONE=1',
' unset PROMPT_COMMAND',
' set -u',
' exec -a figterm-test bash --noprofile --norc -l -i',
'fi',
''
].join('\n')
)
const output = runInteractiveBashRcfile(
getBashShellReadyRcfileContent(),
userDataPath,
'printf \'NOUNSET_EXEC_PC=%s\\n\' "$PROMPT_COMMAND"\nexit 0\n'
)
expect(output).toContain('NOUNSET_EXEC_PC=__orca_exec_osc133_precmd')
expect(output).not.toContain('unbound variable')
})
itWithBash('flattens a prompt array replaced after a failed exec', async () => {
const { getBashShellReadyRcfileContent } = await importFreshLocalPtyShellReady()
writeFileSync(
join(userDataPath, '.bash_profile'),
[
'PROMPT_COMMAND=(\'printf "ARRAY_ZERO\\n"\' \'printf "ARRAY_ONE\\n"\')',
'shopt -s execfail',
'exec /orca-missing-replacement 2>/dev/null',
'PROMPT_COMMAND=(\'printf "ARRAY_ZERO\\n"\' \'printf "ARRAY_ONE\\n"\')',
''
].join('\n')
)
const output = runInteractiveBashRcfile(
getBashShellReadyRcfileContent(),
userDataPath,
'exit 0\n'
)
expect(output.split('ARRAY_ZERO')).toHaveLength(2)
expect(output.split('ARRAY_ONE')).toHaveLength(2)
expect(output).not.toContain('__orca_exec_osc133_precmd: command not found')
})
itWithBash('preserves user-enabled functrace and nested DEBUG trap behavior', async () => {
const { getBashShellReadyRcfileContent } = await importFreshLocalPtyShellReady()
writeFileSync(