Prompt regress (will not pass yet).

This commit is contained in:
Your Name
2026-06-25 12:40:49 +01:00
parent e5fb88fb85
commit 3d26aa0dfb
2 changed files with 636 additions and 0 deletions

300
regress/prompt-keys.sh Normal file
View File

@@ -0,0 +1,300 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT="$TEST_TMUX -Ltest -f/dev/null" # outer (host for the client)
IN="$TEST_TMUX -Ltest2 -f/dev/null" # inner (under test)
$OUT kill-server 2>/dev/null
$IN kill-server 2>/dev/null
trap "$OUT kill-server 2>/dev/null; $IN kill-server 2>/dev/null" EXIT
fail() {
echo "[FAIL] $1"
exit 1
}
# Capture the outer pane: what the inner client rendered. No -e, so we match
# plain visible text, not styles or escape sequences.
capture() {
$OUT capture-pane -p
}
# The "(search) ..." prompt row of a mode prompt.
search_row() {
capture | grep '(search)' | head -1
}
# The inner status line is the last row of the outer capture (status is at the
# bottom). Used to assert a prompt is in the pane and not on the status line.
status_line() {
capture | tail -1
}
# Inner mode must still be a tree mode (the prompt did not close or crash it).
in_tree_mode() {
[ "$($IN display-message -p '#{pane_mode}')" = "tree-mode" ]
}
# Small settle for the key -> inner server -> inner client -> outer pane round
# trip to redraw before we capture. Matches the short waits other regress tests
# use; we do not depend on exact timing beyond the redraw completing.
settle() {
sleep 0.5
}
# Assert the mode search prompt currently shows exactly "(search) <want>".
search_is() {
want=$1; msg=$2
search_row | grep -qF "(search) $want" || \
fail "$msg (wanted '(search) $want', got '$(search_row)')"
}
# --- Inner session under test. ---------------------------------------------
#
# Two windows so the tree has content; status on so we can distinguish the pane
# from the status line; fixed size and manual sizing so the layout is stable.
# A root-table key opens a status-line command prompt whose accept action writes
# the final buffer into @r, so we can recover it exactly.
$IN new -d -x80 -y24 "sh -c 'exec sleep 1000'" || exit 1
$IN set -g status on || exit 1
$IN set -g status-position bottom || exit 1
$IN set -g status-keys emacs || exit 1
$IN set -g window-size manual || exit 1
$IN new-window -d "sh -c 'exec sleep 1000'" || exit 1
$IN bind -n M-r command-prompt -p ">" "set -g @r '%%'" || exit 1
# --- Outer session: attach the inner one inside its pane. -------------------
$OUT new -d -x80 -y24 || exit 1
$OUT set -g status off || exit 1
$OUT set -g window-size manual || exit 1
$OUT send-keys -l "$IN attach" || exit 1
$OUT send-keys Enter || exit 1
sleep 1
# ===========================================================================
# Mode prompt (choose-tree search): the thorough engine vehicle.
# ===========================================================================
$IN choose-tree || exit 1
settle
in_tree_mode || fail "choose-tree did not enter tree-mode"
# --- 1. Search prompt is drawn in the pane, not on the status line. ---
$IN send-keys C-s || exit 1
settle
search_row | grep -q '(search)' || fail "search prompt not drawn in the pane"
status_line | grep -q '(search)' && \
fail "search prompt drawn on the status line, not in the pane"
# --- 2. emacs editing: insert and delete at middle, start and end. ---
# Cursor position is checked behaviourally: move, insert a marker, read the row.
# This needs no cursor coordinates and fails if a movement/edit key is wrong.
# Middle insert: "abcd", Left Left (cursor between b and c), insert X -> abXcd.
$IN send-keys -l "abcd" || exit 1
settle
search_is "abcd" "literal input not shown in search prompt"
$IN send-keys Left Left || exit 1
$IN send-keys -l "X" || exit 1
settle
search_is "abXcd" "middle insert wrong (Left/insert)"
# Middle delete: BSpace removes X (before cursor), DC removes c (at cursor).
$IN send-keys BSpace || exit 1
$IN send-keys DC || exit 1
settle
search_is "abd" "middle delete wrong (BSpace/Delete)"
# Clear, then start/end insert with C-a and C-e.
$IN send-keys C-u || exit 1
$IN send-keys -l "mno" || exit 1
$IN send-keys C-a || exit 1
$IN send-keys -l "S" || exit 1
$IN send-keys C-e || exit 1
$IN send-keys -l "E" || exit 1
settle
search_is "SmnoE" "C-a/C-e start/end insert wrong"
# Word kill: "hello world", C-w removes the last word leaving "hello " (with the
# separating space). capture-pane trims trailing spaces, so make the space
# visible by inserting a marker after it: the buffer becomes "hello Z".
$IN send-keys C-u || exit 1
$IN send-keys -l "hello world" || exit 1
$IN send-keys C-w || exit 1
$IN send-keys -l "Z" || exit 1
settle
search_is "hello Z" "C-w did not kill a word"
# C-a then C-k kills the whole line.
$IN send-keys C-a || exit 1
$IN send-keys C-k || exit 1
settle
search_row | grep -q '(search) [^ ]' && fail "C-a C-k did not clear the line"
# --- 3. Editing kept the prompt open the whole time. ---
in_tree_mode || fail "editing keys closed the mode"
search_row | grep -q '(search)' || fail "editing keys closed the prompt"
# --- 4. Unicode wide character: insert, render, delete as one unit. ---
$IN send-keys C-u || exit 1
$IN send-keys -l "a中b" || exit 1
settle
search_is "a中b" "wide character not shown"
# Left moves over "b" (one column); BSpace deletes the wide "中" as a single
# width-2 unit, leaving "ab".
$IN send-keys Left || exit 1
$IN send-keys BSpace || exit 1
settle
search_is "ab" "wide character not deleted as one unit"
# --- 5. Control character: quote-next inserts it literally, shown as ^G. ---
$IN send-keys C-u || exit 1
$IN send-keys -l "a" || exit 1
$IN send-keys C-v || exit 1 # quote next key
$IN send-keys C-g || exit 1 # literal BEL -> displayed as ^G
$IN send-keys -l "b" || exit 1
settle
search_is "a^Gb" "control character not shown as ^G"
# Deleted as a single unit too.
$IN send-keys Left || exit 1
$IN send-keys BSpace || exit 1
settle
search_is "ab" "control character not deleted as one unit"
# --- 6. Kill and yank: C-w fills the yank buffer, C-y pastes it at the cursor. ---
# (prompt_key only fills the yank buffer from C-w; C-y then yanks that text, or
# the top paste buffer if nothing has been killed. Establish our own kill here
# so the result is deterministic.)
$IN send-keys C-u || exit 1
$IN send-keys -l "one two" || exit 1
$IN send-keys C-w || exit 1 # kill "two", buffer "one "
$IN send-keys C-y || exit 1 # yank it back -> "one two"
settle
search_is "one two" "C-y did not yank the killed text"
$IN send-keys C-y || exit 1 # yank again at cursor -> "one twotwo"
settle
search_is "one twotwo" "second C-y did not yank again"
# --- 7. History: accept a string, reopen, Up recalls it. ---
$IN send-keys C-u || exit 1
$IN send-keys -l "alpha" || exit 1
$IN send-keys Enter || exit 1
settle
$IN send-keys C-s || exit 1
settle
$IN send-keys Up || exit 1
settle
search_is "alpha" "history (Up) did not recall the previous entry"
# --- 8. Escape closes the prompt but leaves the mode open. ---
$IN send-keys Escape || exit 1
settle
search_row | grep -q '(search)' && fail "Escape left a dangling search prompt"
in_tree_mode || fail "Escape closed the mode as well as the prompt"
# Leave the mode.
$IN send-keys q || exit 1
settle
# ===========================================================================
# Status-line prompt (command-prompt): the same engine on the status line.
# Keys go through the inner client's terminal (outer send-keys); the accepted
# buffer is recovered exactly via %% -> @r.
# ===========================================================================
# --- 10. Prompt is drawn on the status line (the last row). ---
$IN set -g @r "" || exit 1
$OUT send-keys M-r || exit 1
settle
status_line | grep -q '>' || fail "status-line prompt not drawn on the status line"
# --- 11. emacs cursor-marker edit, accept recovers the exact buffer. ---
$OUT send-keys -l "abc" || exit 1
$OUT send-keys Home || exit 1
$OUT send-keys -l "X" || exit 1
settle
status_line | grep -qF "> Xabc" || \
fail "status-line edit wrong (got '$(status_line)')"
$OUT send-keys Enter || exit 1
settle
[ "$($IN show -gv @r)" = "Xabc" ] || \
fail "status-line accept recovered '$($IN show -gv @r)', wanted 'Xabc'"
# --- 12. Unicode on the status line: insert, move, delete wide char. ---
$IN set -g @r "" || exit 1
$OUT send-keys M-r || exit 1
settle
$OUT send-keys -l "a㋡b" || exit 1
settle
status_line | grep -qF "a㋡b" || \
fail "status-line wide character not shown (got '$(status_line)')"
# Home, insert Z (start); End, BSpace (delete b); BSpace (delete wide char).
$OUT send-keys Home || exit 1
$OUT send-keys -l "Z" || exit 1
$OUT send-keys End || exit 1
$OUT send-keys BSpace || exit 1
$OUT send-keys BSpace || exit 1
settle
$OUT send-keys Enter || exit 1
settle
[ "$($IN show -gv @r)" = "Za" ] || \
fail "status-line wide edit recovered '$($IN show -gv @r)', wanted 'Za'"
# --- 13. Overflow: more text than fits stays within the line and is kept. ---
big="0123456789012345678901234567890123456789012345678901234567890123456789ABCDEFGHIJ"
$IN set -g @r "" || exit 1
$OUT send-keys M-r || exit 1
settle
$OUT send-keys -l "$big" || exit 1
settle
# The drawn status line must not exceed the client width (80): no wrap, no crash.
width=$(status_line | awk '{print length($0)}')
[ "$width" -le 80 ] || fail "overflowing prompt drew $width columns, wider than 80"
$OUT send-keys Enter || exit 1
settle
# The whole buffer was kept despite only part being visible.
[ "$($IN show -gv @r)" = "$big" ] || fail "overflowing prompt lost buffer content"
# --- 14. Escape closes the status-line prompt cleanly. ---
$IN set -g @r "SENTINEL" || exit 1
$OUT send-keys M-r || exit 1
settle
$OUT send-keys -l "discard" || exit 1
$OUT send-keys Escape || exit 1
settle
status_line | grep -q '> discard' && fail "Escape left a dangling status-line prompt"
[ "$($IN show -gv @r)" = "SENTINEL" ] || fail "Escape ran the prompt's accept action"
# ===========================================================================
# Two clients attached to the same window: a mode prompt must render on both.
# ===========================================================================
$OUT new-window || exit 1
$OUT set -g status off || exit 1
$OUT send-keys -l "$IN attach" || exit 1
$OUT send-keys Enter || exit 1
sleep 1
$IN choose-tree || exit 1
settle
$IN send-keys C-s || exit 1
settle
$IN send-keys -l "dual" || exit 1
settle
for w in $($OUT list-windows -F '#{window_index}'); do
$OUT capture-pane -t ":$w" -p | grep -qF "(search) dual" || \
fail "mode prompt not shown on client in outer window $w"
done
$IN send-keys Escape || exit 1
settle
# --- Inner tmux is still alive and responsive. ---
$IN display-message -p '#{version}' >/dev/null 2>&1 || fail "inner tmux died"
exit 0

336
regress/prompt-mechanics.sh Normal file
View File

@@ -0,0 +1,336 @@
#!/bin/sh
# Exercise the prompt mechanics shared by all three host paths of the prompt
# engine in prompt.c:
#
# status.c status_prompt_set - the status-line command prompt.
# window.c window_pane_set_prompt - a prompt drawn over a pane (-P).
# mode-tree.c mode_tree_set_prompt - search/filter prompts in tree modes.
#
# prompt-keys.sh covers the editing keys; this test covers that each path OPENS
# and DRAWS in the right place and that the prompt flags select the right engine
# behaviour: -1 (single), -N (numeric), -i (incremental), -k (key), -e
# (backspace exit), -I (prefill), multi prompts, type-scoped history and command
# completion.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT="$TEST_TMUX -Ltest -f/dev/null" # outer (host for the client)
IN="$TEST_TMUX -Ltest2 -f/dev/null" # inner (under test)
$OUT kill-server 2>/dev/null
$IN kill-server 2>/dev/null
trap "$OUT kill-server 2>/dev/null; $IN kill-server 2>/dev/null" EXIT
fail() {
echo "[FAIL] $1"
exit 1
}
# Capture the outer pane: what the inner client rendered. Pane-area prompts
# (over a pane or in a tree mode) and the status-line prompt all end up here.
capture() {
$OUT capture-pane -p
}
# The inner status line is the last row of the outer capture.
status_line() {
capture | tail -1
}
# The recovered buffer. Every prompt below accepts into the @r option so the
# exact final string can be checked; reset it to a sentinel first so we can tell
# "accept ran" from "prompt cancelled".
got() {
$IN show -gv @r
}
reset() {
$IN set -g @r "SENTINEL" || exit 1
}
# Settle for the key -> inner server -> inner client -> outer pane redraw round
# trip, as in prompt-keys.sh.
settle() {
sleep 0.5
}
# --- Inner session under test. ---------------------------------------------
#
# The window is created at -y23 so that, with a one-row bottom status, the pane
# area (23) exactly fills the 24-row client: a pane prompt drawn on the pane's
# bottom row lands on visible row 23, just above the status line on row 24. A
# second, distinctively named window gives the tree something to filter.
$IN new -d -x80 -y23 -n YAK "sh -c 'exec sleep 1000'" || exit 1
$IN set -g status on || exit 1
$IN set -g status-position bottom || exit 1
$IN set -g status-keys emacs || exit 1
$IN set -g window-size manual || exit 1
$IN new-window -d -n ZEBRA "sh -c 'exec sleep 1000'" || exit 1
# One root-table key per path/flag. The accept template records the final
# buffer in @r (or both buffers, for the multi-prompt case).
$IN bind -n M-s command-prompt -p '(stat)' "set -g @r '%%'" || exit 1
$IN bind -n M-p command-prompt -P -p '(pane)' "set -g @r '%%'" || exit 1
$IN bind -n M-o command-prompt -P -1 -p '(one)' "set -g @r '%%'" || exit 1
$IN bind -n M-n command-prompt -P -N -I 5 -p '(num)' "set -g @r '%%'" || exit 1
$IN bind -n M-i command-prompt -P -i -p '(inc)' "set -g @r '%%'" || exit 1
$IN bind -n M-k command-prompt -P -k -p '(key)' "set -g @r '%%'" || exit 1
$IN bind -n M-e command-prompt -P -e -p '(bs)' "set -g @r '%%'" || exit 1
$IN bind -n M-j command-prompt -P -I hello -p '(pre)' "set -g @r '%%'" || exit 1
$IN bind -n M-m command-prompt -p 'first,second' "set -g @r '%1/%2'" || exit 1
$IN bind -n M-c command-prompt -p '(cmd)' "set -g @r '%%'" || exit 1
$IN bind -n M-h command-prompt -T search -p '(srch)' "set -g @r '%%'" || exit 1
# --- Outer session: attach the inner one inside its pane. -------------------
$OUT new -d -x80 -y24 || exit 1
$OUT set -g status off || exit 1
$OUT set -g window-size manual || exit 1
$OUT send-keys -l "$IN attach" || exit 1
$OUT send-keys Enter || exit 1
sleep 1
# ===========================================================================
# 1. Each path opens and draws in the right place.
# ===========================================================================
# --- 1a. status.c: drawn on the status line (the last row). ---
reset
$OUT send-keys M-s || exit 1
settle
status_line | grep -qF '(stat)' || \
fail "status-line prompt not on the status line (got '$(status_line)')"
$OUT send-keys -l "go" || exit 1
$OUT send-keys Enter || exit 1
settle
[ "$(got)" = "go" ] || fail "status-line accept recovered '$(got)', wanted 'go'"
# --- 1b. window.c: drawn over the pane, not on the status line. ---
reset
$OUT send-keys M-p || exit 1
settle
capture | grep -qF '(pane)' || fail "pane prompt not drawn in the pane"
status_line | grep -qF '(pane)' && \
fail "pane prompt drawn on the status line, not over the pane"
$OUT send-keys -l "deep" || exit 1
$OUT send-keys Enter || exit 1
settle
[ "$(got)" = "deep" ] || fail "pane prompt accept recovered '$(got)', wanted 'deep'"
# --- 1c. mode-tree.c: search prompt drawn in the pane. ---
$IN choose-tree || exit 1
settle
[ "$($IN display-message -p '#{pane_mode}')" = "tree-mode" ] || \
fail "choose-tree did not enter tree-mode"
$IN send-keys C-s || exit 1
settle
capture | grep -qF '(search)' || fail "mode-tree search prompt not drawn in the pane"
status_line | grep -qF '(search)' && \
fail "mode-tree search prompt drawn on the status line"
$IN send-keys Escape || exit 1
settle
# --- 1d. mode-tree.c: filter prompt opens, applies, prefills, and clears. ---
$IN send-keys f || exit 1
settle
capture | grep -qF '(filter)' || fail "mode-tree filter prompt not drawn"
$IN send-keys -l "ZEBRA" || exit 1
$IN send-keys Enter || exit 1
settle
capture | grep -q 'filter: active' || fail "accepting the filter did not apply it"
# Reopening the filter prompt prefills it with the current filter.
$IN send-keys f || exit 1
settle
capture | grep -qF '(filter) ZEBRA' || \
fail "filter prompt not prefilled with the current filter"
$IN send-keys Escape || exit 1
settle
# 'c' clears the filter.
$IN send-keys c || exit 1
settle
capture | grep -q 'filter: active' && fail "'c' did not clear the filter"
$IN send-keys q || exit 1
settle
# ===========================================================================
# 2. Flags select the right engine behaviour.
# ===========================================================================
# --- 2a. -1 (PROMPT_SINGLE): one keystroke closes and accepts that char. ---
reset
$OUT send-keys M-o || exit 1
settle
capture | grep -qF '(one)' || fail "single prompt did not open"
$OUT send-keys -l "q" || exit 1
settle
[ "$(got)" = "q" ] || fail "single prompt recovered '$(got)', wanted 'q'"
capture | grep -qF '(one)' && fail "single prompt stayed open after one key"
# --- 2b. -N (PROMPT_NUMERIC): prefilled, digits append, non-digit closes. ---
reset
$OUT send-keys M-n || exit 1
settle
capture | grep -qF '(num) 5' || fail "numeric prompt not prefilled with 5"
$OUT send-keys -l "7" || exit 1 # 57
$OUT send-keys Enter || exit 1 # Enter is a non-digit: close
settle
[ "$(got)" = "57" ] || fail "numeric accept recovered '$(got)', wanted '57'"
# A non-digit key closes the prompt with the existing buffer, dropping the key.
reset
$OUT send-keys M-n || exit 1
settle
$OUT send-keys -l "x" || exit 1
settle
[ "$(got)" = "5" ] || fail "numeric non-digit close recovered '$(got)', wanted '5'"
capture | grep -qF '(num)' && fail "numeric prompt stayed open after a non-digit"
# --- 2c. -i (PROMPT_INCREMENTAL): callback fires on every edit, stays open. ---
# The incremental code path prefixes the buffer with '=' (or +/-), so the '='
# proves the value came through the incremental callback, not a plain accept.
reset
$OUT send-keys M-i || exit 1
settle
[ "$(got)" = "=" ] || fail "incremental prompt did not fire on open (got '$(got)')"
$OUT send-keys -l "a" || exit 1
settle
[ "$(got)" = "=a" ] || fail "incremental did not fire after 'a' (got '$(got)')"
$OUT send-keys -l "b" || exit 1
settle
[ "$(got)" = "=ab" ] || fail "incremental did not fire after 'b' (got '$(got)')"
capture | grep -qF '(inc)' || fail "incremental prompt closed during editing"
$OUT send-keys Escape || exit 1
settle
capture | grep -qF '(inc)' && fail "Escape did not close the incremental prompt"
# --- 2d. -k (PROMPT_KEY): the next key closes and delivers its name. ---
reset
$OUT send-keys M-k || exit 1
settle
capture | grep -qF '(key)' || fail "key prompt did not open"
$OUT send-keys -l "z" || exit 1
settle
[ "$(got)" = "z" ] || fail "key prompt recovered '$(got)', wanted 'z'"
capture | grep -qF '(key)' && fail "key prompt stayed open after a key"
# --- 2e. -e (PROMPT_BSPACE_EXIT): backspace on empty cancels (no accept). ---
reset
$OUT send-keys M-e || exit 1
settle
$OUT send-keys BSpace || exit 1
settle
[ "$(got)" = "SENTINEL" ] || fail "backspace-exit ran the accept action (got '$(got)')"
capture | grep -qF '(bs)' && fail "backspace on empty did not close the prompt"
# --- 2f. -I (prefill): prompt opens with the given buffer. ---
reset
$OUT send-keys M-j || exit 1
settle
capture | grep -qF '(pre) hello' || fail "prefill not shown (got '$(capture | grep -F '(pre)')')"
$OUT send-keys Enter || exit 1
settle
[ "$(got)" = "hello" ] || fail "prefill accept recovered '$(got)', wanted 'hello'"
# --- 2g. Multi prompt: accept advances to the next, both are delivered. ---
reset
$OUT send-keys M-m || exit 1
settle
capture | grep -qF 'first' || fail "first of multi prompt not shown"
$OUT send-keys -l "X" || exit 1
$OUT send-keys Enter || exit 1
settle
capture | grep -qF 'second' || fail "multi prompt did not advance to the second"
$OUT send-keys -l "Y" || exit 1
$OUT send-keys Enter || exit 1
settle
[ "$(got)" = "X/Y" ] || fail "multi prompt recovered '$(got)', wanted 'X/Y'"
# ===========================================================================
# 3. Type-scoped history and command completion.
# ===========================================================================
# --- 3a. Command history is recalled with Up; search history is separate. ---
reset
$OUT send-keys M-c || exit 1
settle
$OUT send-keys -l "alpha" || exit 1
$OUT send-keys Enter || exit 1
settle
[ "$(got)" = "alpha" ] || fail "command prompt accept recovered '$(got)'"
# Reopen the command prompt: Up recalls the command-type entry.
$OUT send-keys M-c || exit 1
settle
$OUT send-keys Up || exit 1
settle
capture | grep -qF '(cmd) alpha' || fail "Up did not recall command history"
$OUT send-keys Escape || exit 1
settle
# A search-type prompt must NOT recall the command-type entry (separate rings).
$OUT send-keys M-h || exit 1
settle
$OUT send-keys Up || exit 1
settle
capture | grep -qF 'alpha' && fail "search prompt recalled command-type history"
$OUT send-keys Escape || exit 1
settle
# --- 3b. Tab completion works in a command prompt (command type only). ---
$OUT send-keys M-c || exit 1
settle
$OUT send-keys -l "new-w" || exit 1
$OUT send-keys Tab || exit 1
settle
status_line | grep -qF 'new-window' || \
fail "Tab did not complete new-w to new-window (got '$(status_line)')"
$OUT send-keys Escape || exit 1
settle
# A search-type prompt does not complete commands: Tab is literal / inert.
$OUT send-keys M-h || exit 1
settle
$OUT send-keys -l "new-w" || exit 1
$OUT send-keys Tab || exit 1
settle
status_line | grep -qF 'new-window' && fail "search prompt completed a command"
$OUT send-keys Escape || exit 1
settle
# ===========================================================================
# 4. Robustness: re-entrancy guard and Escape, then liveness.
# ===========================================================================
# --- 4a. A second prompt while one is open is refused (status path). ---
client=$($IN list-clients -F '#{client_name}' | head -1)
reset
$OUT send-keys M-s || exit 1
settle
$OUT send-keys -l "AAA" || exit 1
settle
$IN command-prompt -t"$client" -p '(re)' "set -g @r 'REENTERED'" 2>/dev/null
settle
capture | grep -qF '(re)' && fail "a second status prompt opened over the first"
status_line | grep -qF '(stat) AAA' || fail "first status prompt was disturbed"
$OUT send-keys Escape || exit 1
settle
[ "$(got)" = "SENTINEL" ] || fail "Escape ran the status prompt accept action"
# --- 4b. A second pane prompt while one is open is refused (pane path). ---
reset
$OUT send-keys M-p || exit 1
settle
$OUT send-keys -l "BBB" || exit 1
settle
$IN command-prompt -P -t"$client" -p '(re)' "set -g @r 'REENTERED'" 2>/dev/null
settle
capture | grep -qF '(re)' && fail "a second pane prompt opened over the first"
capture | grep -qF '(pane) BBB' || fail "first pane prompt was disturbed"
$OUT send-keys Escape || exit 1
settle
[ "$(got)" = "SENTINEL" ] || fail "Escape ran the pane prompt accept action"
# --- 4c. Inner tmux survived every path and flag. ---
$IN display-message -p '#{version}' >/dev/null 2>&1 || fail "inner tmux died"
exit 0