Files
stack/docs/plans/reviews/2026-09-07_tmux-r2-export/tools/tmux/test-send-message-verdict.sh
T

343 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# test-send-message-verdict.sh — locks the fail-loud verdict logic of the patched
# send-message.sh against three real tmux-pane fixtures on a throwaway socket:
#
# 1. DELIVERED — a REPL that renders a ` ` input box and submits on Enter
# (text scrolls to history, box clears) => exit 0 "✓ delivered".
# 2. UNCONFIRMED — a pane with NO locatable prompt glyph. This is the exact
# historical FALSE POSITIVE: pre-patch it printed "✓ delivered"
# exit 0; post-patch it MUST fail loud (exit 2, stderr
# "could not confirm submission").
# 3. DRAFT — a ` `-prompt pane that never submits (message stays on the
# input line) => exit 2, stderr "unsubmitted draft".
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
# (box shape) anywhere (pi's shape) and which submits => exit 0. Pre-#1362
# the glyph probe could not see this box at all, so EVERY send
# to such a pane reported "may be UNDELIVERED" while landing.
# 5. DRAFT — the same glyphless box, holding our tail across every flush
# (box shape) Enter => exit 2, stderr "unsubmitted draft". Pre-#1362 this
# also reported unconfirmed, so the true state was invisible.
# 6. DELIVERED — the pi box whose TOP rule transiently renders with a trailing
# (em-dash rule) em dash (measured live 2026-09-07): the strict rule regex
# rejected the top rule, leaving a single-rule "box not
# locatable" => false UNDELIVERED alarm on every such send.
# Post-fix: exit 0 ✓ delivered.
# 7. UNCONFIRMED — a pane with no locatable box at all: re-captures must NOT
# (no re-Enter) re-send blind Enters. Fixture counts received Enters; after
# a send with -r 1 the count must be exactly 1 (the single
# submit Enter). Pre-fix it was 2 (Enter before every capture).
# 8. PASTE FAIL — both paste attempts fail: abort loud (exit 2, "paste ..."
# stderr) BEFORE any Enter, buffer discarded — never a bare
# Enter sequence that could read as "delivered" while empty.
set -uo pipefail
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
SEND="$HERE/send-message.sh"
SOCKET="verdict-test-$RANDOM-$$"
TMP=$(mktemp -d)
trap 'tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
command -v tmux >/dev/null 2>&1 || { echo "tmux required" >&2; exit 1; }
# --- Fixture 1: a submitting REPL with a prompt box (interactive bash, glyph PS1).
# readline strips bracketed-paste markers just like a real agent REPL; Enter
# executes (text -> scrollback), leaving a fresh empty ` ` box.
tmux -L "$SOCKET" new-session -d -s repl -c "$TMP" \
'PS1=" " exec bash --noprofile --norc -i'
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: -prompt REPL that submits => exit 0 ✓ delivered"
else
no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
fi
# --- Fixture 2: NO prompt glyph (default bash PS1). THE regression: pre-patch this
# was a silent false-positive "delivered"; post-patch it must be unconfirmed→exit 2.
tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); then
no "unconfirmed: glyphless pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "could not confirm submission" "$TMP/e2"; then
ok "unconfirmed: glyphless pane => exit 2 + 'could not confirm submission' (false-positive FIXED)"
else
no "unconfirmed: glyphless pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e2")]"
fi
fi
# --- Fixture 3: a box that never submits (sleep ignores stdin; TTY echo keeps the
# pasted tail sitting on the line) => draft => exit 2.
tmux -L "$SOCKET" new-session -d -s draft -c "$TMP" \
'printf " "; exec sleep infinity'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=draft" -r 1 -m "verdict fixture three stuck unsubmitted draft" 2>"$TMP/e3"); then
no "draft: unsubmitted message must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e3"; then
ok "draft: stuck -line message => exit 2 + 'unsubmitted draft'"
else
no "draft: stuck -line message => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e3")]"
fi
fi
# --- Fixtures 4 and 5: a pi-shaped pane. The input box is two `─` rules with the
# text between them and NO prompt glyph anywhere, so the glyph probe alone can
# never locate it and every send reports "may be UNDELIVERED" (#1362). The
# renderer below is the shape, not the runtime: MODE=clear submits (box empties),
# MODE=keep leaves the text sitting in the box.
cat > "$TMP/pibox.sh" <<'PIBOX'
#!/usr/bin/env bash
MODE=${1:-clear}
RULE=$(printf '─%.0s' $(seq 1 60))
history=""
buf=""
draw() {
printf '\033[H\033[2J'
printf 'fixture output line\n%s\n' "$history"
printf '%s\n' "$RULE"
printf '%s\n' "$buf"
printf '%s\n' "$RULE"
printf '~/fixture (main)\n'
printf 'tok 0 model fixture\n'
}
draw
while IFS= read -r line; do
# keep: hold the tail across every flush Enter, which is what a stuck draft does.
if [ "$MODE" = keep ]; then [ -n "$line" ] && buf=$line; else history+="$line"; buf=""; fi
draw
done
PIBOX
chmod +x "$TMP/pibox.sh"
tmux -L "$SOCKET" new-session -d -s pibox -c "$TMP" "exec bash '$TMP/pibox.sh' clear"
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=pibox" -m "pi fixture four delivered ok" 2>"$TMP/e4"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered"
else
no "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e4")]"
fi
tmux -L "$SOCKET" new-session -d -s piboxdraft -c "$TMP" "exec bash '$TMP/pibox.sh' keep"
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=piboxdraft" -r 1 -m "pi fixture five stuck in the box" 2>"$TMP/e5"); then
no "draft: glyphless box-drawn pane holding our tail must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e5"; then
ok "draft: message left in a glyphless box => exit 2 + 'unsubmitted draft'"
else
no "draft: message left in a glyphless box => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e5")]"
fi
fi
# --- Fixture 6: pi box whose top rule carries a trailing em dash (transient
# redraw shape measured live on pi, 2026-09-07). Only the top rule differs
# from fixture 4's renderer.
cat > "$TMP/piboxdash.sh" <<'PIBOXDASH'
#!/usr/bin/env bash
RULE=$(printf '─%.0s' $(seq 1 60))
history=""
buf=""
draw() {
printf '\033[H\033[2J'
printf 'fixture output line\n%s\n' "$history"
printf '%s—\n' "$RULE"
printf '%s\n' "$buf"
printf '%s\n' "$RULE"
printf '~/fixture (main)\n'
}
draw
while IFS= read -r line; do
history+="$line"
buf=""
draw
done
PIBOXDASH
chmod +x "$TMP/piboxdash.sh"
tmux -L "$SOCKET" new-session -d -s piboxdash -c "$TMP" "exec bash '$TMP/piboxdash.sh'"
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=piboxdash" -m "em dash fixture six delivered ok" 2>"$TMP/e6"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: top rule with trailing em dash => exit 0 ✓ delivered"
else
no "delivered: top rule with trailing em dash => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e6")]"
fi
# --- Fixture 7: no locatable box anywhere; count Enters the pane receives.
# The single submit Enter is expected; re-captures must stay silent.
cat > "$TMP/counter.sh" <<'COUNTER'
#!/usr/bin/env bash
n=0
: > "$1"
while IFS= read -r _line; do
n=$((n + 1))
printf '%s' "$n" > "$1"
done
COUNTER
chmod +x "$TMP/counter.sh"
tmux -L "$SOCKET" new-session -d -s counter -c "$TMP" "exec bash '$TMP/counter.sh' '$TMP/enters'"
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=counter" -r 1 -m "fixture seven unconfirmable" 2>"$TMP/e7"); then
no "unconfirmed: unlocatable pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
enters=$(cat "$TMP/enters" 2>/dev/null || echo 0)
if [ "$rc" -eq 2 ] && [ "$enters" = "1" ]; then
ok "unconfirmed: unlocatable pane => exit 2 with exactly 1 Enter (no blind re-submits)"
else
no "unconfirmed: unlocatable pane => exit 2 with exactly 1 Enter" "rc=$rc enters=$enters err=[$(cat "$TMP/e7")]"
fi
fi
# --- Fixture 8: paste attempts fail (stubbed tmux refuses paste-buffer, passes
# everything else through to the real binary). Must abort BEFORE any Enter:
# exit 2, stderr names the paste failure, counter stays at zero.
FAKE_BIN8="$TMP/fakebin8"; mkdir -p "$FAKE_BIN8"
REAL_TMUX8=$(command -v tmux)
cat > "$FAKE_BIN8/tmux" <<TMUX8
#!/usr/bin/env bash
case " \$* " in
*" paste-buffer "*) exit 1 ;; # send-message invokes: tmux -L <sock> paste-buffer ...
esac
exec "$REAL_TMUX8" "\$@"
TMUX8
chmod +x "$FAKE_BIN8/tmux"
tmux -L "$SOCKET" new-session -d -s pastefail -c "$TMP" "exec bash '$TMP/counter.sh' '$TMP/enters8'"
sleep 0.3
: > "$TMP/enters8"
if out=$(PATH="$FAKE_BIN8:$PATH" "$SEND" -L "$SOCKET" -t "=pastefail" -m "fixture eight never pastes" 2>"$TMP/e8"); then
no "paste-fail: failed paste must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
enters8=$(cat "$TMP/enters8" 2>/dev/null); enters8=${enters8:-0}
if [ "$rc" -eq 2 ] && grep -qF "paste into" "$TMP/e8" && [ "$enters8" = "0" ]; then
ok "paste-fail: failed paste => exit 2 loud, zero Enters sent"
else
no "paste-fail: failed paste => exit 2 loud, zero Enters" "rc=$rc enters8=$enters8 err=[$(cat "$TMP/e8")]"
fi
fi
# Transport failures must not be upgraded by a stale queued banner or prompt.
mkdir -p "$TMP/faultbin"
cat > "$TMP/faultbin/tmux" <<'FAULTMUX'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$FAULT_LOG"
case " $* " in
*" load-buffer "*) cat >/dev/null; [ "$FAULT_OP" != load-buffer ]; exit $? ;;
*" send-keys "*) [ "$FAULT_OP" != send-keys ]; exit $? ;;
*" capture-pane "*) printf 'Press up to edit queued messages\n \n' ;;
esac
exit 0
FAULTMUX
chmod +x "$TMP/faultbin/tmux"
for op in load-buffer send-keys; do
: > "$TMP/fault-log"
out=$(PATH="$TMP/faultbin:$PATH" FAULT_LOG="$TMP/fault-log" FAULT_OP="$op" \
"$SEND" -L fixture -t '=fault' -m 'transport fault test' 2>"$TMP/fault-err"); rc=$?
if [ "$rc" -eq 2 ] && [ -z "$out" ] && [ "$(grep -c capture-pane "$TMP/fault-log")" -eq 1 ]; then
ok "$op failure refuses after baseline without post-send confirmation"
else
no "$op failure must refuse after baseline only" "rc=$rc out=[$out]"
fi
done
# Historical success-looking text without a current input must fail closed.
for fixture in banner history rules adjacent capture; do
mkdir -p "$TMP/historybin"
cat > "$TMP/historybin/tmux" <<'HISTORYMUX'
#!/usr/bin/env bash
case " $* " in
*" load-buffer "*) cat >/dev/null ;;
*" capture-pane "*)
if [ "$HISTORY_FIXTURE" = banner ]; then
printf 'Press up to edit queued messages\n'
elif [ "$HISTORY_FIXTURE" = capture ]; then
printf ' \n'; exit 1
elif [ "$HISTORY_FIXTURE" = adjacent ]; then
printf '────────\n────────\n~/fixture (main)\n'
elif [ "$HISTORY_FIXTURE" = rules ]; then
printf 'historical output\n────────\nold text\n────────\nmore output; no current editor\n'
else
printf ' old prompt\nsubsequent output without an input box\n'
fi ;;
esac
exit 0
HISTORYMUX
chmod +x "$TMP/historybin/tmux"
out=$(PATH="$TMP/historybin:$PATH" HISTORY_FIXTURE="$fixture" \
"$SEND" -L fixture -t '=history' -m 'new unrelated message' 2>"$TMP/history-err"); rc=$?
if [ "$rc" -eq 2 ] && [ -z "$out" ]; then
ok "historical $fixture cannot confirm a new message"
else
no "historical $fixture must remain unconfirmed" "rc=$rc out=[$out]"
fi
done
# Retained Unicode and whitespace-wrapped messages must remain drafts in C locale.
mkdir -p "$TMP/unicodebin"
cat > "$TMP/unicodebin/tmux" <<'UNICODEMUX'
#!/usr/bin/env bash
case " $* " in
*" load-buffer "*) cat >/dev/null ;;
*" send-keys "*) printf 'Enter\n' >> "$ENTER_LOG" ;;
*" capture-pane "*) printf '────────\n%s\n────────\n~/fixture (main)\n' "$RENDERED_DRAFT" ;;
esac
exit 0
UNICODEMUX
chmod +x "$TMP/unicodebin/tmux"
for shape in unicode wrapped; do
if [ "$shape" = unicode ]; then body='你好世界'; rendered=$'你好\n世界';
else body=$'alpha beta\ngamma delta'; rendered=$'alpha\nbeta gamma\ndelta'; fi
: > "$TMP/draft-enters"
out=$(LC_ALL=C PATH="$TMP/unicodebin:$PATH" RENDERED_DRAFT="$rendered" ENTER_LOG="$TMP/draft-enters" \
"$SEND" -L fixture -t '=unicode' -r 0 -m "$body" 2>"$TMP/unicode-err"); rc=$?
if [ "$rc" -eq 2 ] && [ -z "$out" ] && [ "$(wc -l < "$TMP/draft-enters")" -eq 1 ]; then
ok "retained $shape message refuses with only initial submission key"
else
no "retained $shape message must not confirm" "rc=$rc out=[$out]"
fi
done
mkdir -p "$TMP/transitionbin"
cat > "$TMP/transitionbin/tmux" <<'TRANSITIONMUX'
#!/usr/bin/env bash
case " $* " in
*" load-buffer "*) cat >/dev/null ;;
*" capture-pane "*)
n=$(cat "$CAPTURE_COUNT"); n=$((n + 1)); printf '%s' "$n" > "$CAPTURE_COUNT"
if [ "$TRANSITION" = repeated ] || { [ "$TRANSITION" = new ] && [ "$n" -gt 1 ]; }; then
printf 'unique-correlation-message\n'
fi
printf '────────\n\n────────\n~/fixture (main)\n' ;;
esac
exit 0
TRANSITIONMUX
chmod +x "$TMP/transitionbin/tmux"
for scenario in static repeated new; do
printf '0' > "$TMP/capture-count"
expected=2; [ "$scenario" = new ] && expected=0
out=$(LC_ALL=C PATH="$TMP/transitionbin:$PATH" TRANSITION="$scenario" CAPTURE_COUNT="$TMP/capture-count" \
"$SEND" -L fixture -t '=transition' -m unique-correlation-message 2>"$TMP/transition-err"); rc=$?
if [ "$rc" -eq "$expected" ]; then
ok "$scenario message visibility transition => exit $expected"
else
no "$scenario transition" "rc=$rc expected=$expected out=[$out]"
fi
done
echo "---"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]