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]>
This commit is contained in:
@@ -32,9 +32,7 @@
|
|||||||
# 0 delivered (submitted) or queued (agent busy; will process when free)
|
# 0 delivered (submitted) or queued (agent busy; will process when free)
|
||||||
# 1 tmux target not found
|
# 1 tmux target not found
|
||||||
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
|
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
|
||||||
# input box could not be located to confirm the message actually landed.
|
# input prompt could not be located to confirm the message actually landed.
|
||||||
# Locating the box is runtime-specific; see locate_input_box() below, and
|
|
||||||
# add a shape there before pointing this tool at a new runtime.
|
|
||||||
# Delivery is NEVER inferred from absence of evidence: if we cannot positively
|
# Delivery is NEVER inferred from absence of evidence: if we cannot positively
|
||||||
# see the input box clear of the message (or the queued banner), we fail loud
|
# see the input box clear of the message (or the queued banner), we fail loud
|
||||||
# so the sender learns immediately instead of a silent worker->lead stall.
|
# so the sender learns immediately instead of a silent worker->lead stall.
|
||||||
@@ -99,53 +97,34 @@ printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -
|
|||||||
# would otherwise accumulate forever.
|
# would otherwise accumulate forever.
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
|
||||||
# Locate the REPL input box in a captured pane. Prints the box's contents on
|
# 2) Submit, then POSITIVELY confirm submission by DRAFT TRANSITION, not by prompt
|
||||||
# stdout and returns 0 when the box was FOUND; returns 1 when it could not be
|
# glyph. The historical bug was treating ABSENCE of a draft as delivery; the
|
||||||
# located at all. Found-but-empty is a real, distinct answer (an empty input box
|
# 2026-08 fix over-corrected to glyph inference (grep '❯|^>|│ >'), which locates
|
||||||
# is what a submitted message leaves behind), so the caller must branch on the
|
# only Claude Code's box and false-NEGATIVES every glyphless REPL (pi renders a
|
||||||
# return code, never on whether the output is empty.
|
# U+2500 rule, no glyph) — a delivered message reported "UNDELIVERED", driving a
|
||||||
#
|
# retry that duplicates it. Runtime-agnostic evidence: our message tail sits on
|
||||||
# Two REPL shapes are recognised:
|
# the INPUT line (located by the cursor row, not a glyph) BEFORE Enter, and has
|
||||||
# * a prompt-glyph line — `❯`, a leading `>`, or `│ >`. Claude Code and most
|
# LEFT it AFTER — that transition is positive proof of submission and needs no
|
||||||
# readline REPLs.
|
# glyph. Absence alone still never means delivered: if we never saw our draft on
|
||||||
# * a box drawn as two horizontal `─` rules with the input between them and NO
|
# the input line we stay UNCONFIRMED (wrong/dead pane), and a draft that never
|
||||||
# prompt glyph anywhere. pi renders this. Anchoring on the LAST rule pair is
|
# leaves the input line stays a DRAFT (exit 2), preserving both historical guards.
|
||||||
# what makes it safe: agent output can contain its own rules, but nothing is
|
_cursor_line() { # echo the pane's current input (cursor) line, glyph-free
|
||||||
# drawn below the input box except the status line.
|
local cy line
|
||||||
#
|
cy=$("${tmux_cmd[@]}" display-message -p -t "$EFFECTIVE_TARGET" -F '#{cursor_y}' 2>/dev/null) || return 1
|
||||||
# Adding a runtime means adding its shape HERE. A shape that is missing does not
|
[ -n "$cy" ] || return 1
|
||||||
# degrade gracefully: it turns every send to that runtime into a false
|
"${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null | sed -n "$((cy + 1))p"
|
||||||
# "may be UNDELIVERED", which is what #1362 measured on pi and #1257 on another
|
}
|
||||||
# arm of the same probe.
|
_draft_on_input() { # true iff our message tail is sitting on the input line now
|
||||||
locate_input_box() {
|
[ -n "$snippet" ] || return 1
|
||||||
local pane=$1 glyph_line rule_lines top bottom
|
grep -qF "$snippet" <<<"$(_cursor_line)"
|
||||||
glyph_line=$(printf '%s\n' "$pane" | grep -E '❯|^>|│ >' | tail -1)
|
|
||||||
if [ -n "$glyph_line" ]; then printf '%s\n' "$glyph_line"; return 0; fi
|
|
||||||
rule_lines=$(printf '%s\n' "$pane" | grep -nE '^[[:space:]]*─{4,}[[:space:]]*$' | cut -d: -f1 | tail -2)
|
|
||||||
[ -n "$rule_lines" ] || return 1
|
|
||||||
# Split the (at most two) captured line numbers with parameter expansion. Not
|
|
||||||
# `head -1`: piping into an early-exiting consumer SIGPIPEs the producer, which
|
|
||||||
# under `set -euo pipefail` aborts the caller with rc=141 and no output. The
|
|
||||||
# scripts/pipefail-early-exit.test.mjs guard reds on that shape, correctly.
|
|
||||||
# With one rule captured both halves resolve to the same value and the
|
|
||||||
# ordering test below rejects it, which is the answer we want anyway.
|
|
||||||
top=${rule_lines%%$'\n'*}
|
|
||||||
bottom=${rule_lines##*$'\n'}
|
|
||||||
[ "$top" != "$bottom" ] || return 1
|
|
||||||
[ "$bottom" -gt "$top" ] || return 1
|
|
||||||
# An empty range (adjacent rules) prints nothing and still returns 0: found,
|
|
||||||
# empty, which is the delivered shape.
|
|
||||||
printf '%s\n' "$pane" | sed -n "$((top + 1)),$((bottom - 1))p"
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2) Submit, then POSITIVELY confirm submission; flush with another Enter if it is
|
# Baseline: after the paste, our draft must be on the input line. This is positive
|
||||||
# still a draft. Success requires positive evidence — the queued banner, OR the
|
# proof we are on the right pane and the paste landed — the anchor the transition
|
||||||
# REPL input box located AND clear of our message tail. The historical bug was
|
# check measures against.
|
||||||
# treating ABSENCE of a draft as delivery: if the input box was never located
|
saw_draft=0
|
||||||
# (wrong pane / prompt-glyph drift), an unsubmitted message read as "delivered"
|
_draft_on_input && saw_draft=1
|
||||||
# and worker->lead relays stalled silently. We now default to UNCONFIRMED and only
|
|
||||||
# upgrade to delivered on positive evidence; anything we cannot confirm fails loud.
|
|
||||||
status="unconfirmed"
|
status="unconfirmed"
|
||||||
for attempt in $(seq 1 $((RETRIES + 1))); do
|
for attempt in $(seq 1 $((RETRIES + 1))); do
|
||||||
"${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter
|
"${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter
|
||||||
@@ -155,19 +134,26 @@ for attempt in $(seq 1 $((RETRIES + 1))); do
|
|||||||
if grep -qF "$QUEUED_RE" <<<"$pane"; then
|
if grep -qF "$QUEUED_RE" <<<"$pane"; then
|
||||||
status="queued"; break
|
status="queued"; break
|
||||||
fi
|
fi
|
||||||
# If we cannot see the input box, we have NO evidence of submission state —
|
# POSITIVE draft evidence from a located prompt box, when one exists. This is the
|
||||||
# stay UNCONFIRMED and retry; never infer delivery.
|
# cursor-row check's blind spot: a pane in COOKED mode (a plain shell whose
|
||||||
if ! inputbox=$(locate_input_box "$pane"); then
|
# foreground process never reads stdin) echoes our paste via the kernel line
|
||||||
status="unconfirmed"; continue
|
# discipline and moves the cursor off it on Enter, which is indistinguishable from
|
||||||
fi
|
# a real submit by cursor row alone. If a prompt box IS locatable and still carries
|
||||||
# Input box located AND still carrying our tail => unsubmitted draft. Flush + retry.
|
# our tail, that is affirmative proof the message was not consumed. Absence of a
|
||||||
# (Submitted messages scroll up into history; a draft stays in the box.)
|
# glyph is still never used for anything — that inference is the original E7 bug.
|
||||||
if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$inputbox"; then
|
promptline=$(printf '%s' "$pane" | grep -E '❯|^>|│ >' | tail -1)
|
||||||
|
if [ -n "$promptline" ] && [ -n "$snippet" ] && grep -qF "$snippet" <<<"$promptline"; then
|
||||||
status="draft"; continue
|
status="draft"; continue
|
||||||
fi
|
fi
|
||||||
# Input box located AND clear of our tail => positively submitted. This is the
|
if [ "$saw_draft" = 1 ]; then
|
||||||
# only path to success besides the queued banner.
|
if _draft_on_input; then
|
||||||
status="delivered"; break
|
status="draft"; continue # still on the input line => not submitted; flush + retry
|
||||||
|
fi
|
||||||
|
status="delivered"; break # left the input line => positively submitted
|
||||||
|
fi
|
||||||
|
# No confirmed baseline yet: try to (re)acquire it; never infer delivery from absence.
|
||||||
|
if _draft_on_input; then saw_draft=1; status="draft"; continue; fi
|
||||||
|
status="unconfirmed"; continue
|
||||||
done
|
done
|
||||||
|
|
||||||
[ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; }
|
[ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; }
|
||||||
@@ -176,6 +162,6 @@ case "$status" in
|
|||||||
delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
|
delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
|
||||||
queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
|
queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;;
|
||||||
draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
|
draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;;
|
||||||
unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input box not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
|
unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input prompt not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
|
||||||
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
|
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/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 ]
|
||||||
@@ -4,10 +4,13 @@
|
|||||||
#
|
#
|
||||||
# 1. DELIVERED — a REPL that renders a `❯ ` input box and submits on Enter
|
# 1. DELIVERED — a REPL that renders a `❯ ` input box and submits on Enter
|
||||||
# (text scrolls to history, box clears) => exit 0 "✓ delivered".
|
# (text scrolls to history, box clears) => exit 0 "✓ delivered".
|
||||||
# 2. UNCONFIRMED — a pane with NO locatable prompt glyph. This is the exact
|
# 2. DELIVERED — a pane with NO prompt glyph that DOES submit => exit 0. A pi
|
||||||
# historical FALSE POSITIVE: pre-patch it printed "✓ delivered"
|
# seat is this fixture (U+2500 rule, no glyph). Reshaped for
|
||||||
# exit 0; post-patch it MUST fail loud (exit 2, stderr
|
# #1257; see the note at the fixture for why the old exit-2
|
||||||
# "could not confirm submission").
|
# 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
|
# 3. DRAFT — a `❯ `-prompt pane that never submits (message stays on the
|
||||||
# input line) => exit 2, stderr "unsubmitted draft".
|
# input line) => exit 2, stderr "unsubmitted draft".
|
||||||
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
|
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
|
||||||
@@ -44,19 +47,44 @@ else
|
|||||||
no "delivered: ❯-prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
|
no "delivered: ❯-prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Fixture 2: NO prompt glyph (default bash PS1). THE regression: pre-patch this
|
# --- Fixture 2: NO prompt glyph, and the pane DOES submit (interactive bash).
|
||||||
# was a silent false-positive "delivered"; post-patch it must be unconfirmed→exit 2.
|
# 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" \
|
tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \
|
||||||
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
|
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
|
||||||
sleep 0.3
|
sleep 0.3
|
||||||
if out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); then
|
out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); rc=$?
|
||||||
no "unconfirmed: glyphless pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
|
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
|
else
|
||||||
rc=$?
|
rc=$?
|
||||||
if [ "$rc" -eq 2 ] && grep -qF "could not confirm submission" "$TMP/e2"; then
|
if [ "$rc" -ne 0 ] && grep -qF "could not confirm submission" "$TMP/e2b"; then
|
||||||
ok "unconfirmed: glyphless pane => exit 2 + 'could not confirm submission' (false-positive FIXED)"
|
ok "unconfirmed: glyphless hung TUI (raw/no-echo) => non-zero + 'could not confirm submission'"
|
||||||
else
|
else
|
||||||
no "unconfirmed: glyphless pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e2")]"
|
no "unconfirmed: glyphless hung TUI => non-zero + stderr" "rc=$rc err=[$(cat "$TMP/e2b")]"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user