a73x

cbf1a48f

test: the e2e suite stops depending on a fast reap, a pager's height and a $TMPDIR without a trailing slash

a73x   2026-09-04 10:16

Commit message
test: the e2e suite stops depending on a fast reap, a pager's height and a $TMPDIR without a trailing slash

Three more assumptions that held on Linux and not on macOS 26.

The reap leg read the predecessor's stranded agent directory straight off
a bound socket. The reap is not a boot step — it happens inside the
successor's own makeDir, when its first session is born — so the check
raced the thing it was asserting, and lost on a Mac. It waits for the
successor's own directory to appear and reads the predecessor's then.

The pager leg spelled `less +G`, which says "put the end of the file on
screen" and leaves the top wherever the build's status line puts it: 178
under less 704, 179 under less 668. `+178g` names the top row instead, so
the wheel's 24 rows are asserted against a number the pager was told
rather than one it chose.

macOS sets $TMPDIR to a per-user directory ending in `/`, so every path
this suite spells came out with `//` in it, and the two groups that
compare such a path as a string — a layout leaf against a bound socket, a
hosts line against the daemon attached to — failed on a spelling. The
slash is stripped once, before the first of the 191 call sites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

test/e2e_01_boot.sh
Old New
@@ -417,6 +417,21 @@ AGENTLEFT=$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3DEAD-*" |
417 echo "e2e FAIL: a SIGKILLed daemon left $AGENTLEFT agent directories for pid $D3DEAD (want 1)"; exit 1; } 417 echo "e2e FAIL: a SIGKILLed daemon left $AGENTLEFT agent directories for pid $D3DEAD (want 1)"; exit 1; }
418 start_daemon "$SOCK3" "$OUT.d3b.d" "daemon did not rebind the stale socket" --shell /bin/sh 418 start_daemon "$SOCK3" "$OUT.d3b.d" "daemon did not rebind the stale socket" --shell /bin/sh
419 D3PID=$DPID 419 D3PID=$DPID
420 # The reap is not a boot step: it happens inside the successor's own
421 # `makeDir`, which runs when its FIRST session is born — here, when the
422 # stranded client above redials. A bound socket says nothing about that,
423 # so wait for the successor's own directory to appear and read the
424 # predecessor's only then. Asserting straight off start_daemon read a
425 # directory that was removed microseconds later, which is a race Linux
426 # won every time and a Mac lost (2026-09-04).
427 _i=0
428 until [ -n "$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3PID-*")" ]; do
429 _i=$((_i + 1)); [ "$_i" -lt $(( 100 * TIME_SCALE )) ] || {
430 echo "e2e FAIL: the restarted daemon never made an agent directory of its own,"
431 echo " so nothing here has reaped yet and the check below would lie:"
432 find "$(dirname "$SOCK3")" -maxdepth 1 -name 'mux-agent-*'; exit 1; }
433 sleep 0.1
434 done
420 AGENTLEFT=$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3DEAD-*" | wc -l) 435 AGENTLEFT=$(find "$(dirname "$SOCK3")" -maxdepth 1 -name "mux-agent-$D3DEAD-*" | wc -l)
421 [ "$AGENTLEFT" -eq 0 ] || { 436 [ "$AGENTLEFT" -eq 0 ] || {
422 echo "e2e FAIL: the restarted daemon left its SIGKILLed predecessor's agent directory:" 437 echo "e2e FAIL: the restarted daemon left its SIGKILLed predecessor's agent directory:"
test/e2e_08_mouse.sh
Old New
@@ -199,19 +199,19 @@ ok "an application that asked for the mouse gets the wheel, and the client does
199 # consumed by the mouse filter and then dropped. Measured on this very 199 # consumed by the mouse filter and then dropped. Measured on this very
200 # scenario before the fix: four notches, grid unmoved. 200 # scenario before the fix: four notches, grid unmoved.
201 # 201 #
202 # `less +G` on 200 lines: the view starts at the END, so a wheel-UP is the 202 # `less +178g` on 200 lines: line 178 is put at the TOP of the screen, so a
203 # direction with somewhere to go. Eight notches at less's default of three 203 # wheel-UP has 177 rows to go and the leg knows where it started. Eight
204 # rows a notch is 24 rows, one screenful, so the top row moves back by 24 204 # notches at less's default of three rows a notch is 24 rows, one
205 # and the row that was at the top leaves the screen entirely — a needle and 205 # screenful, so the top moves to 154 and 178 leaves the screen entirely —
206 # its negative. 206 # a needle and its negative.
207 # 207 #
208 # The two row numbers are READ off the grid before the wheel rather than 208 # `+G` is what this leg used to spell, and it is the one thing here that
209 # written here, because how many of the 24 rows a pager spends on its own 209 # cannot be written down: it says "put the END of the file on screen", and
210 # status line is the pager's business and differs between builds: less 704 210 # where the top of that view lands depends on how many of the 24 rows the
211 # (Debian) starts this file at 178 and less 668 (macOS 26) at 179, and a 211 # build spends on its own status line. less 704 (Debian) left the top at
212 # leg that spelled 178 and 154 failed on the Mac by exactly one row while 212 # 178 and less 668 (macOS 26) at 179, so a leg that spelled 178 and 154
213 # the wheel worked perfectly. What this leg is about is the 24, not where 213 # failed on the Mac by exactly one row while the wheel worked perfectly.
214 # the pager chose to begin. 214 # `g` names the top row instead of deriving it, and both builds obey it.
215 # 215 #
216 # LESS is cleared in the script rather than trusted: an operator with 216 # LESS is cleared in the script rather than trusted: an operator with
217 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse, 217 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse,
@@ -222,34 +222,24 @@ cat > "$LESSSH" <<EOF
222 #!/bin/sh 222 #!/bin/sh
223 LESS= 223 LESS=
224 export LESS 224 export LESS
225 exec less +G $LESSDATA 225 exec less +178g $LESSDATA
226 EOF 226 EOF
227 chmod +x "$LESSSH" 227 chmod +x "$LESSSH"
228 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH" 228 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH"
229 D32PID=$DPID 229 D32PID=$DPID
230 # Not wait_grid: the needle is anchored (`-qx`, a row that is EXACTLY the 230 # Not wait_grid: the needle is anchored (`-qx`, a row that is EXACTLY one
231 # file's last line, so a half-painted `2` does not pass) and the dump is 231 # of the file's lines, so a half-painted `19` does not pass) and the dump
232 # trimmed to the last rows, because a pager's whole grid is 24 rows of 232 # is trimmed to the last rows, because a pager's whole grid is 24 rows of
233 # numbers that say nothing about why it stopped short. 233 # numbers that say nothing about why it stopped short. 190 rather than the
234 # top row 178: it is well inside the page either build draws, so the needle
235 # does not depend on the height question the comment above settles.
234 i=0 236 i=0
235 until "$MUX" d dump --sock "$SOCK35" | grep -qx "200"; do 237 until "$MUX" d dump --sock "$SOCK35" | grep -qx "190"; do
236 i=$((i+1)); [ "$i" -lt $(( 100 * TIME_SCALE )) ] || { 238 i=$((i+1)); [ "$i" -lt $(( 100 * TIME_SCALE )) ] || {
237 echo "e2e FAIL: pager never reached the end of the file:" 239 echo "e2e FAIL: pager never painted the page it was sent to:"
238 "$MUX" d dump --sock "$SOCK35" | tail -3; exit 1; } 240 "$MUX" d dump --sock "$SOCK35" | tail -3; exit 1; }
239 sleep 0.1 241 sleep 0.1
240 done 242 done
241 # The row the pager put at the top, read now and not written down: the
242 # daemon's session is 80x24 and the ptyclient below attaches at 80x24, so
243 # nothing between here and the wheel moves the view.
244 PGRTOP=$("$MUX" d dump --sock "$SOCK35" | sed -n '1p')
245 case "$PGRTOP" in
246 ''|*[!0-9]*)
247 echo "e2e FAIL: pager: the top row of the settled grid is not a line number: '$PGRTOP'"
248 "$MUX" d dump --sock "$SOCK35" | head -3; exit 1 ;;
249 esac
250 [ "$PGRTOP" -gt 24 ] || {
251 echo "e2e FAIL: pager: top row $PGRTOP leaves no room for a screenful of wheel"
252 "$MUX" d dump --sock "$SOCK35" | head -3; exit 1; }
253 set +e 243 set +e
254 hostroom pager 244 hostroom pager
255 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \ 245 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \
@@ -271,13 +261,12 @@ rc0 "pager: ptyclient leg exited $RC:" "$OUT.pgr.log"
271 "$MUX" d dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1 261 "$MUX" d dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1
272 # A whole line, because the pager's status line carries the file's PATH — 262 # A whole line, because the pager's status line carries the file's PATH —
273 # which holds this run's pid and could spell any short number. 263 # which holds this run's pid and could spell any short number.
274 PGRWANT=$(( PGRTOP - 24 )) 264 grep -qx "154" "$OUT.pgrcap" || {
275 grep -qx "$PGRWANT" "$OUT.pgrcap" || { 265 echo "e2e FAIL: pager: the wheel did not move the alternate screen back a screenful"
276 echo "e2e FAIL: pager: the wheel did not move the alternate screen back 24 rows" 266 echo " (top was 178, so row 154 should be on screen):"
277 echo " (top was $PGRTOP, so row $PGRWANT should be on screen):"
278 cat "$OUT.pgrcap"; exit 1; } 267 cat "$OUT.pgrcap"; exit 1; }
279 grep -qx "$PGRTOP" "$OUT.pgrcap" && { 268 grep -qx "178" "$OUT.pgrcap" && {
280 echo "e2e FAIL: pager: the old top row $PGRTOP is still on screen, so the view did not move a screenful:" 269 echo "e2e FAIL: pager: the old top row is still on screen, so the view did not move a screenful:"
281 cat "$OUT.pgrcap"; exit 1; } 270 cat "$OUT.pgrcap"; exit 1; }
282 # The pager never asked for the mouse, so it must never have SEEN a mouse 271 # The pager never asked for the mouse, so it must never have SEEN a mouse
283 # report: what reached it was arrow keys, which leave no text behind. 272 # report: what reached it was arrow keys, which leave no text behind.
test/e2e_lib.sh
Old New
@@ -12,6 +12,26 @@
12 # What is NOT here: the scenario count. ok() counts, and the pin that 12 # What is NOT here: the scenario count. ok() counts, and the pin that
13 # gates the count is the runner's, at the bottom of test/e2e.sh — a group 13 # gates the count is the runner's, at the bottom of test/e2e.sh — a group
14 # file knows nothing about how many scenarios the suite has. 14 # file knows nothing about how many scenarios the suite has.
15 # ---- $TMPDIR, trailing slash removed -----------------------------------
16 # Every temporary path in this suite is spelled `${TMPDIR:-/tmp}/name`, and
17 # a good few of them are then compared as STRINGS — a layout leaf against
18 # the socket the daemon bound, a hosts line against the daemon the client
19 # attached to. macOS sets $TMPDIR to a per-user directory ending in `/`, so
20 # those paths came out with `//` in the middle: mux records the spelling it
21 # was handed, the suite spells its expectation the same way, and the two
22 # still differ when only one of them went through a normalizing step. Two
23 # groups failed on exactly that. Stripped once here rather than at 191 call
24 # sites, and before the first of them.
25 while :; do
26 case "${TMPDIR:-}" in
27 */) TMPDIR="${TMPDIR%/}" ;;
28 *) break ;;
29 esac
30 done
31 # An empty $TMPDIR is not a directory; leave it unset so `${TMPDIR:-/tmp}`
32 # falls back the way every caller here expects.
33 [ -n "${TMPDIR:-}" ] && export TMPDIR || unset TMPDIR
34
15 # ---- the cleanup registry --------------------------------------------- 35 # ---- the cleanup registry ---------------------------------------------
16 # Every process and every artifact a leg creates is REGISTERED where it is 36 # Every process and every artifact a leg creates is REGISTERED where it is
17 # created, and the EXIT trap walks the registers. Before this, cleanup() 37 # created, and the EXIT trap walks the registers. Before this, cleanup()