Files
stack/packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh
T
veronica dc6db5f14e
ci/woodpecker/pr/ci Pipeline failed
fix(tmux): locate the REPL input box by shape, not by a Claude-only glyph
send-message.sh confirmed delivery by grepping the captured pane for a
prompt glyph (`❯`, a leading `>`, or `│ >`). Those are Claude Code shapes.
pi draws its input box as two horizontal `─` rules with the input between
them and no glyph anywhere, so on a pi pane the grep matched nothing, the
`delivered` and `draft` branches were unreachable, and every send exited 2
with "may be UNDELIVERED" after burning all its retries — on messages that
had in fact been delivered.

Measured on five live seats: pi seats vision, tess and medic returned 0
glyph matches on non-empty captures (3653 / 1086 / 1890 bytes); Claude
seats fred and tuesday returned 3 and 1.

This is a different arm from #1257. On the measured host
`tmux capture-pane -t <session> -p` and `-t <session>:0.0 -p` both return
the same 3653 bytes, so #1257's empty-capture mechanism does not reproduce
here; fixing only that would leave every pi send at rc=2 and would verify
green against a Claude pane.

The probe becomes locate_input_box(), which returns a status code instead
of a string. Found-but-empty is a real answer (an empty input box is what
a submitted message leaves behind) and is now distinguishable from
not-found, which the old `[ -z "$promptline" ]` test conflated. Box
detection anchors on the LAST pair of `─` rules: agent output can contain
its own rules, but nothing is drawn below the input box except the status
line.

Adding a runtime means adding its shape in that one function. A missing
shape does not degrade gracefully — it turns every send to that runtime
into a false "may be UNDELIVERED", which is exactly what this fixes.

Tests: two fixtures added to test-send-message-verdict.sh driving a
glyphless box-drawn REPL, one that submits (expect exit 0 delivered) and
one that holds the text in the box (expect exit 2 draft). Patched suite
PASS=5 FAIL=0. Control against the pristine origin/next send-message.sh:
PASS=3 FAIL=2, fixture 4 failing as `rc=2 ... REPL input prompt not
locatable after 3 attempts — message may be UNDELIVERED`, which is the
defect reproduced in a test. Sibling suites unchanged and green:
test-send-message-socket.sh rc=0, agent-send.test.sh PASS=19 FAIL=0.

Note the suite is in test-enumeration-exclusions.txt (the CI image ships
no tmux), so these fixtures are verified locally only. That exclusion and
its #1017 burndown reason are unchanged by this commit.

Not addressed here: auto-submit-drafts.sh lines 20 and 26 carry the same
Claude-only `❯` assumption. Separate surface, separate change.

Closes #1362
2026-08-21 16:54:39 -05:00

132 lines
6.0 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.
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))
buf=""
draw() {
printf '\033[H\033[2J'
printf 'fixture output line\n\n'
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 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
echo "---"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]