a73x

c0471242

test: the pager leg reads its own anchor and asserts exactly 24 rows on both OSes

a73x   2026-09-04 10:16

Commit message
test: the pager leg reads its own anchor and asserts exactly 24 rows on both OSes

The row is exact on purpose: on Linux a wheel that moved 23 rows lands one
row off, and 23 rows is what one lost keystroke out of eight notches looks
like, so a one-row spread would pass exactly the regression this leg exists
to catch.

The anchor is read rather than written down. A short attach that waits for
the paint and detaches leaves the daemon's grid holding the REPAINTED
view, and the dump after it is the top row the wheel is about to move,
taken while nothing is attached and nothing can change it. The assertion
is then row anchor-24 on screen and row anchor gone, which is exactly 24
on either build. Measured three runs on each OS: the anchor is 178 under
less 704 and 179 under less 668, and the wheel lands on 154 and 155.

Waiting longer before the wheel was tested and changes nothing: a 1200 ms
quiet settle between the paint and the first notch gives the same 155 on
the Mac, three runs. The one-row difference is where the repaint puts the
view, not a keystroke going missing.

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

test/e2e_08_mouse.sh
Old New
@@ -199,21 +199,29 @@ 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 +178g` on 200 lines: line 178 is put at the TOP of the screen, so a 202 # `less +G` on 200 lines: the view starts at the END, so a wheel-UP is the
203 # wheel-UP has 177 rows to go and the leg knows where it started. Eight 203 # direction with somewhere to go. Eight notches at less's default of three
204 # notches at less's default of three rows a notch is 24 rows, one 204 # rows a notch is 24 rows, one screenful, so the top row moves back by 24
205 # screenful, so the top moves to 154 and 178 leaves the screen entirely — 205 # and the row that was at the top leaves the screen entirely — a needle and
206 # a needle and its negative. 206 # its negative.
207 # 207 #
208 # The `178g` is typed INTO the pager after the attach, and it is what makes 208 # The top row is READ off the grid rather than written down here, and the
209 # 178 a fact rather than a hope. Attaching makes less repaint, and the 209 # read is taken between two attaches so that nothing is racing it. Where a
210 # repaint does not always land where the flag put it: measured on this 210 # pager parks the end of a file is the pager's business and the builds
211 # scenario, less 704 (Debian) held the top at 178 while less 668 (macOS 26) 211 # disagree: attaching makes less repaint, and how many of the 24 rows the
212 # moved it to 179, so a leg that read the top before the attach, or wrote 212 # build spends on its own status line decides where the repainted view
213 # it down, was off by one row on the Mac while the wheel worked perfectly. 213 # starts. less 704 leaves the top at 178 and less 668, the one macOS 26
214 # Re-anchoring after the repaint leaves nothing for a build to differ 214 # ships, at 179 — measured on this scenario, three runs each. So a leg that
215 # about. The `+178g` flag is still there so the wait loop below has a page 215 # spelled 154 failed on the Mac by one row while the wheel worked
216 # to look for. 216 # perfectly, and one that accepted 154 OR 155 stopped being able to fail on
217 # a wheel that moved 23 rows, which is what a lost keystroke looks like.
218 #
219 # The short attach below is what makes 24 an exact number on both. It
220 # attaches, waits for the paint, and detaches, which leaves the daemon's
221 # grid holding the REPAINTED view; the dump after it is therefore the same
222 # top row the wheel is about to move, read while no client is attached and
223 # nothing can change it. A second attach at the same size repaints to the
224 # same place, measured three times on each OS.
217 # 225 #
218 # LESS is cleared in the script rather than trusted: an operator with 226 # LESS is cleared in the script rather than trusted: an operator with
219 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse, 227 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse,
@@ -224,7 +232,7 @@ cat > "$LESSSH" <<EOF
224 #!/bin/sh 232 #!/bin/sh
225 LESS= 233 LESS=
226 export LESS 234 export LESS
227 exec less +178g $LESSDATA 235 exec less +G $LESSDATA
228 EOF 236 EOF
229 chmod +x "$LESSSH" 237 chmod +x "$LESSSH"
230 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH" 238 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH"
@@ -233,8 +241,8 @@ D32PID=$DPID
233 # of the file's lines, so a half-painted `19` does not pass) and the dump 241 # of the file's lines, so a half-painted `19` does not pass) and the dump
234 # is trimmed to the last rows, because a pager's whole grid is 24 rows of 242 # is trimmed to the last rows, because a pager's whole grid is 24 rows of
235 # numbers that say nothing about why it stopped short. 190 rather than the 243 # numbers that say nothing about why it stopped short. 190 rather than the
236 # top row 178: it is well inside the page either build draws, so the needle 244 # file's last line: it is well inside the page either build draws, so the
237 # does not depend on the height question the comment above settles. 245 # needle does not depend on the height question the comment above settles.
238 i=0 246 i=0
239 until "$MUX" d dump --sock "$SOCK35" | grep -qx "190"; do 247 until "$MUX" d dump --sock "$SOCK35" | grep -qx "190"; do
240 i=$((i+1)); [ "$i" -lt $(( 100 * TIME_SCALE )) ] || { 248 i=$((i+1)); [ "$i" -lt $(( 100 * TIME_SCALE )) ] || {
@@ -242,14 +250,38 @@ until "$MUX" d dump --sock "$SOCK35" | grep -qx "190"; do
242 "$MUX" d dump --sock "$SOCK35" | tail -3; exit 1; } 250 "$MUX" d dump --sock "$SOCK35" | tail -3; exit 1; }
243 sleep 0.1 251 sleep 0.1
244 done 252 done
253 # The anchoring attach. Same size as the one that follows it, because the
254 # thing being fixed is where a repaint at THIS size lands.
245 set +e 255 set +e
246 hostroom pager 256 hostroom pager
257 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \
258 --out "$OUT.pgranchor" --err "$OUT.pgranchor.err" \
259 -- "$MUX" --sock "$SOCK35" > "$OUT.pgranchor.log" 2>&1 <<'EOF'
260 expect 199 15000
261 settle 800 15000
262 send \x1cd
263 waitexit 10000
264 EOF
265 RC=$?
266 set -e
267 rc0 "pager: the anchoring attach exited $RC:" "$OUT.pgranchor.log"
268 PGRTOP=$("$MUX" d dump --sock "$SOCK35" | sed -n '1p')
269 # Refused rather than trusted: an anchor that is not a line number, or one
270 # with no room for a screenful above it, would make the arithmetic below
271 # assert nothing. Both would mean the pager is not where this leg thinks.
272 case "$PGRTOP" in
273 ''|*[!0-9]*)
274 echo "e2e FAIL: pager: the top row after the anchoring attach is not a line number: '$PGRTOP'"
275 "$MUX" d dump --sock "$SOCK35" | head -3; exit 1 ;;
276 esac
277 [ "$PGRTOP" -gt 24 ] || {
278 echo "e2e FAIL: pager: top row $PGRTOP leaves no room for a screenful of wheel"
279 "$MUX" d dump --sock "$SOCK35" | head -3; exit 1; }
280
281 set +e
247 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \ 282 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \
248 -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF' 283 -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF'
249 expect 199 15000 284 expect 199 15000
250 settle 400 15000
251 send 178g
252 settle 600 15000
253 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M 285 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M
254 settle 800 15000 286 settle 800 15000
255 send \x1cd 287 send \x1cd
@@ -266,12 +298,12 @@ rc0 "pager: ptyclient leg exited $RC:" "$OUT.pgr.log"
266 "$MUX" d dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1 298 "$MUX" d dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1
267 # A whole line, because the pager's status line carries the file's PATH — 299 # A whole line, because the pager's status line carries the file's PATH —
268 # which holds this run's pid and could spell any short number. 300 # which holds this run's pid and could spell any short number.
269 grep -qx "154" "$OUT.pgrcap" || { 301 grep -qx "$(( PGRTOP - 24 ))" "$OUT.pgrcap" || {
270 echo "e2e FAIL: pager: the wheel did not move the alternate screen back a screenful" 302 echo "e2e FAIL: pager: the wheel did not move the alternate screen back 24 rows"
271 echo " (top was 178, so row 154 should be on screen):" 303 echo " (the top was $PGRTOP, so row $(( PGRTOP - 24 )) should be on screen):"
272 cat "$OUT.pgrcap"; exit 1; } 304 cat "$OUT.pgrcap"; exit 1; }
273 grep -qx "178" "$OUT.pgrcap" && { 305 grep -qx "$PGRTOP" "$OUT.pgrcap" && {
274 echo "e2e FAIL: pager: the old top row is still on screen, so the view did not move a screenful:" 306 echo "e2e FAIL: pager: the old top row $PGRTOP is still on screen, so the view did not move a screenful:"
275 cat "$OUT.pgrcap"; exit 1; } 307 cat "$OUT.pgrcap"; exit 1; }
276 # The pager never asked for the mouse, so it must never have SEEN a mouse 308 # The pager never asked for the mouse, so it must never have SEEN a mouse
277 # report: what reached it was arrow keys, which leave no text behind. 309 # report: what reached it was arrow keys, which leave no text behind.