349 lines
15 KiB
Bash
Executable File
349 lines
15 KiB
Bash
Executable File
#!/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 — synthetic pi-like box with a trailing em dash on its top
|
||
# (em-dash rule) rule (not an established live rendering): the strict 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: synthetic pi-like box with a trailing em dash on its top rule.
|
||
# This does not establish a live rendering cause. 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=$?
|
||
reason_ok=0
|
||
case "$scenario" in
|
||
static) grep -qF 'reason=new-message-not-visible' "$TMP/transition-err" && reason_ok=1 ;;
|
||
repeated) grep -qF 'reason=message-present-in-baseline' "$TMP/transition-err" && reason_ok=1 ;;
|
||
new) reason_ok=1 ;;
|
||
esac
|
||
if [ "$rc" -eq "$expected" ] && [ "$reason_ok" -eq 1 ]; then
|
||
ok "$scenario message visibility transition => exit $expected with expected content-free diagnostic"
|
||
else
|
||
no "$scenario transition" "rc=$rc expected=$expected out=[$out]"
|
||
fi
|
||
done
|
||
|
||
echo "---"
|
||
echo "PASS=$PASS FAIL=$FAIL"
|
||
[ "$FAIL" -eq 0 ]
|