ci/woodpecker/pr/ci Pipeline failed
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]>
103 lines
5.3 KiB
Bash
Executable File
103 lines
5.3 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. DELIVERED — a pane with NO prompt glyph that DOES submit => exit 0. A pi
|
||
# seat is this fixture (U+2500 rule, no glyph). Reshaped for
|
||
# #1257; see the note at the fixture for why the old exit-2
|
||
# assertion was wrong.
|
||
# 2b. UNCONFIRMED— a glyphless pane that never submits (raw/no-echo hung TUI)
|
||
# => must fail loud. This carries the historical
|
||
# false-positive guard that fixture 2 used to be credited with.
|
||
# 3. DRAFT — a `❯ `-prompt pane that never submits (message stays on the
|
||
# input line) => exit 2, stderr "unsubmitted draft".
|
||
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 ] && printf '%s' "$out" | grep -qF "✓ delivered"; 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, and the pane DOES submit (interactive bash).
|
||
# RESHAPED 2026-08-16 (#1257), deliberately. This fixture previously asserted
|
||
# exit 2 here and was labelled "false-positive FIXED". That assertion was wrong,
|
||
# and locking it in is what kept E7 alive: the pane submits, so "delivered" is
|
||
# the truth, and a pi seat — whose input box is a bare U+2500 rule with no glyph
|
||
# — IS this fixture. Reporting exit 2 for it told operators a delivered message
|
||
# may be undelivered, and the retry that advice invites is the duplicate.
|
||
#
|
||
# The guard this fixture was reaching for is real and is NOT dropped: "never
|
||
# infer delivered from absence" is now enforced positively by fixture 2b below
|
||
# (glyphless AND not submitting => must fail) and by fixture 3 (locatable box
|
||
# still carrying our tail => draft). Absence alone decides nothing either way.
|
||
tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \
|
||
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
|
||
sleep 0.3
|
||
out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); rc=$?
|
||
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qF "✓ delivered"; then
|
||
ok "delivered: glyphless pane that submits => exit 0 (runtime-agnostic, E7 FIXED)"
|
||
else
|
||
no "delivered: glyphless pane that submits => exit 0" "rc=$rc out=[$out] err=[$(cat "$TMP/e2")]"
|
||
fi
|
||
|
||
# --- Fixture 2b: NO prompt glyph AND never submits — a hung managed TUI holding the
|
||
# terminal in raw/no-echo, which is what a stuck agent seat actually is (measured
|
||
# on live pi: stty -echo -icanon). Nothing is echoed, nothing is consumed, so
|
||
# there is no positive evidence of submission and the tool MUST fail loud. This
|
||
# is the historical false-positive guard, kept as a positive test.
|
||
tmux -L "$SOCKET" new-session -d -s rawstuck -c "$TMP" \
|
||
'bash --noprofile --norc -c "stty -echo -icanon min 1 time 0 2>/dev/null; exec sleep infinity"'
|
||
sleep 0.3
|
||
if out=$("$SEND" -L "$SOCKET" -t "=rawstuck" -r 1 -m "verdict fixture two-b never submitted" 2>"$TMP/e2b"); then
|
||
no "unconfirmed: glyphless hung TUI must NOT report success" "expected non-zero, got 0 (out=[$out])"
|
||
else
|
||
rc=$?
|
||
if [ "$rc" -ne 0 ] && grep -qF "could not confirm submission" "$TMP/e2b"; then
|
||
ok "unconfirmed: glyphless hung TUI (raw/no-echo) => non-zero + 'could not confirm submission'"
|
||
else
|
||
no "unconfirmed: glyphless hung TUI => non-zero + stderr" "rc=$rc err=[$(cat "$TMP/e2b")]"
|
||
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
|
||
|
||
echo "---"
|
||
echo "PASS=$PASS FAIL=$FAIL"
|
||
[ "$FAIL" -eq 0 ]
|