ci/woodpecker/pr/ci Pipeline failed
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
132 lines
6.0 KiB
Bash
Executable File
132 lines
6.0 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.
|
||
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 ]
|