Files
stack/packages/mosaic/framework/tools/tmux/test-send-message-glyph-agnostic.sh
T
fredandscooby b09589f02c
ci/woodpecker/pr/ci Pipeline failed
fix(tmux): confirm delivery by draft transition, not by prompt glyph (#1257)
send-message.sh located the REPL input box with grep -E '❯|^>|│ >'. That set is
Claude Code's box. A pi seat renders a bare U+2500 rule with no glyph, so on every
idle pi seat the capture succeeded, the grep matched nothing, status stayed
"unconfirmed", and the tool exited 2 "may be UNDELIVERED" with the paste and the
Enter both landed. The stderr tells the operator to retry, and that retry is the
duplicate delivery reported against the same tool.

Confirmation is now runtime-agnostic: our message tail sits on the input line
(located by cursor row, no glyph) before Enter and has left it after. That
transition is positive proof of submission.

Absence still proves nothing, which is the guard the 2026-08 fix was reaching for
and got backwards. Two positive checks keep it:

  - a prompt box that IS locatable and still carries our tail => draft, exit 2.
    This covers the cursor-row blind spot: a cooked pane whose foreground process
    never reads stdin echoes the paste through the kernel line discipline and
    moves the cursor off it on Enter, which by cursor row alone is indistinguishable
    from a real submit.
  - no draft ever observed on the input line => unconfirmed, non-zero.

Tests, both red-first against the shipping blob d397907:

  test-send-message-glyph-agnostic.sh (new, 6 fixtures)  4/6 -> 6/6
  test-send-message-verdict.sh (fixture 2 reshaped, 2b added)  3/4 -> 4/4

Fixture 2 of the verdict suite asserted exit 2 for a glyphless pane that submits
and was labelled "false-positive FIXED". A pi seat is that fixture, so the suite
was locking the bug in. It is reshaped deliberately, and the guard it was credited
with moves to new fixture 2b (glyphless AND non-submitting, raw/no-echo) so the
"never infer delivered from absence" property is tested positively rather than as
a side effect.

Measured on tmux 3.7b (sb-it-1-dt), 3.5a (fomo-lin), and dragon-lin.

Co-authored-by: scooby <[email protected]>
2026-08-16 16:35:36 -05:00

98 lines
5.2 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
# Red-first regression test for E7 (#1017 task 2): the confirm-check must bind
# "delivered" to WHETHER THE MESSAGE WAS SUBMITTED, not to which runtime's prompt
# glyph is present. A pi seat renders a U+2500 rule input box with no /^>/│ >
# glyph; send-message.sh:118 locates the box only by glyph, so a genuinely
# delivered message on a glyphless REPL falsely reports exit 2 "may be UNDELIVERED",
# and the operator's rc=2-driven retry duplicates it.
#
# Parameterized on $SEND: RED against the shipping blob (B and D fail), GREEN
# against a candidate patch. No pi; no fake HOME; hermetic throwaway socket.
#
# Submission counting is EXACT and terminal-echo-independent: the fixture message
# is `echo <tok> >>SINK`; each real submission appends one line. wc -l SINK ==
# number of times the REPL actually executed the send. This does not depend on how
# many times the marker string is painted on screen.
set -u
SEND="${SEND:?set SEND=/path/to/send-message.sh}"
SOCKET="glyphagnostic-$$"
TMP="$(mktemp -d)"
tmux() { command tmux -L "$SOCKET" "$@"; }
cleanup() { command tmux -L "$SOCKET" kill-server 2>/dev/null; rm -rf "$TMP"; }
trap cleanup EXIT
pass=0; fail=0
ok() { printf 'ok %s\n' "$1"; pass=$((pass+1)); }
no() { printf 'FAIL %s -- %s\n' "$1" "$2"; fail=$((fail+1)); }
mk() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" "PS1='$2' exec bash --noprofile --norc -i"; sleep 0.5; }
subs() { [ -f "$1" ] && wc -l <"$1" | tr -d ' ' || echo 0; } # exact submission count
echo "SEND=$SEND tmux $(command tmux -V | awk '{print $2}')"
# --- A (control): glyph box () that submits => exit 0, exactly one submission.
mk ctl ' '
SINK="$TMP/sink.ctl"
out=$("$SEND" -L "$SOCKET" -t ctl -m "echo x >>'$SINK'" 2>"$TMP/e.ctl"); rc=$?; sleep 0.4
if [ "$rc" = 0 ] && [ "$(subs "$SINK")" = 1 ]; then
ok "control: -box submits => exit 0, exactly one submission"
else no "control: -box submits => exit 0, one submission" "rc=$rc subs=$(subs "$SINK") err=[$(cat "$TMP/e.ctl")]"; fi
# --- B (THE false-rc regression): glyphless U+2500 box that SUBMITS. Message lands
# (subs==1) yet shipping reports exit 2. Must be exit 0.
mk sub $'──────── \n'
SINK="$TMP/sink.sub"
out=$("$SEND" -L "$SOCKET" -t sub -m "echo x >>'$SINK'" 2>"$TMP/e.sub"); rc=$?; sleep 0.4
if [ "$rc" = 0 ] && [ "$(subs "$SINK")" = 1 ]; then
ok "glyphless: U+2500 box that submits => exit 0 (delivered, not 'UNDELIVERED')"
else no "glyphless: U+2500 box that submits => exit 0" \
"rc=$rc subs=$(subs "$SINK")(delivered=$([ "$(subs "$SINK")" -ge 1 ] && echo yes||echo no)) err=[$(cat "$TMP/e.sub")]"; fi
# --- D (duplicate arm): operator follows the rc=2 stderr and retries once. On the
# glyphless box, shipping => two submissions (the reported duplicate). The
# property: one logical send => exactly one submission. Same fix closes it.
mk dup $'──────── \n'
SINK="$TMP/sink.dup"
tries=0
for attempt in 1 2; do
tries=$((tries+1))
out=$("$SEND" -L "$SOCKET" -t dup -m "echo x >>'$SINK'" 2>/dev/null); rc=$?
sleep 0.4
[ "$rc" = 0 ] && break # operator stops retrying only when told delivered
done
if [ "$(subs "$SINK")" = 1 ]; then
ok "duplicate: one logical send (rc-driven retry) => exactly one submission (tries=$tries)"
else no "duplicate: one logical send => exactly one submission" "submissions=$(subs "$SINK") tries=$tries"; fi
# --- E (faithful hung managed TUI, NOT a cooked shell): raw/no-echo, paints nothing.
# A cooked `sleep infinity` echoes the paste via the kernel line discipline and
# false-passes a cursor-row fix that is correct on real seats (measured). So: raw.
mk_rawstuck() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" \
"bash --noprofile --norc -c 'stty -echo -icanon min 1 time 0 2>/dev/null; exec sleep infinity'"; sleep 0.5; }
mk_rawstuck estuck
SINK="$TMP/sink.estuck"
out=$("$SEND" -L "$SOCKET" -t estuck -r 1 -m "this stuck draft was never submitted" 2>/dev/null); rc=$?
sleep 0.3
if [ "$rc" != 0 ] && [ "$(subs "$SINK")" = 0 ]; then
ok "raw/no-echo stuck TUI (not submitted) => non-zero (no false delivered)"
else no "raw stuck TUI must NOT report delivered" "rc=$rc subs=$(subs "$SINK")"; fi
# --- F (busy/queued branch, your BUSY-not-runtime finding): glyphless pane rendering the
# queued banner, never consuming. QUEUED_RE :113 fires before the glyph grep => rc=0.
mk_busy() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" \
"bash --noprofile --norc -c 'printf \"Press up to edit queued messages\n\"; exec sleep infinity'"; sleep 0.5; }
mk_busy ebusy
SINK="$TMP/sink.ebusy"
out=$("$SEND" -L "$SOCKET" -t ebusy -m "echo x >>'$SINK'" 2>/dev/null); rc=$?; sleep 0.3
if [ "$rc" = 0 ]; then
ok "busy/queued-banner glyphless => exit 0 (queued is delivery; runtime owns custody)"
else no "busy/queued-banner must report delivered" "rc=$rc"; fi
# --- C (historical-bug guard): unresolvable target. No pane ever carried our draft
# => must fail, never infer delivered from absence of a glyph/snippet.
if out=$("$SEND" -L "$SOCKET" -t "nonexistent-$$" -m "echo x >>'$TMP/sink.wrong'" 2>/dev/null); then
no "wrong-pane: unresolvable target must NOT report success" "expected non-zero, got 0"
else ok "wrong-pane: unresolvable target => non-zero (no false delivered)"; fi
echo "---"; echo "pass=$pass fail=$fail"
[ "$fail" = 0 ]