a73x

5b97e538

test: 08_mouse moves to a file of its own

a73x   2026-08-26 19:06

Commit message
test: 08_mouse moves to a file of its own

the wheel and the mouse, in a terminal and in a tile — 8 scenarios, 667 lines. The legs move unedited; the runner
sources the file where they used to stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

test/e2e.sh
Old New
@@ -102,7 +102,7 @@ done
102 E2E_DIR=$(dirname "$0") 102 E2E_DIR=$(dirname "$0")
103 . "$E2E_DIR/e2e_lib.sh" 103 . "$E2E_DIR/e2e_lib.sh"
104 104
105 E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli' 105 E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse'
106 # The groups that cannot stand alone, and what they need. E2E_ONLY refuses 106 # The groups that cannot stand alone, and what they need. E2E_ONLY refuses
107 # these by name rather than running them: a group whose fixtures are 107 # these by name rather than running them: a group whose fixtures are
108 # missing does not fail, it passes having tested something else — and a 108 # missing does not fail, it passes having tested something else — and a
@@ -145,670 +145,6 @@ for _g in $E2E_GROUPS; do
145 . "$E2E_DIR/e2e_$_g.sh" 145 . "$E2E_DIR/e2e_$_g.sh"
146 done 146 done
147 147
148 # The wheel pair. Each needs a scrollback of its own to scroll (or to prove
149 # it did not), which the long-lived /bin/sh daemon has no way to hold still,
150 # and the second one needs a session that ASKS for the mouse — a mode that
151 # would follow every other scenario sharing the daemon around.
152 SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock"
153 defer_sock "$SOCK33"
154 WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh"
155 defer_rm "$WHEELSH"
156 SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock"
157 defer_sock "$SOCK34"
158 MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh"
159 defer_rm "$MOUSESH"
160 # The alternate-screen half of the wheel: its own daemon because the
161 # session under test is a PAGER holding the alt screen for its whole life,
162 # which no other scenario could share a grid with. And the no-terminal
163 # half, whose session is a bare `cat` so that what reaches the pty is
164 # echoed back into the grid the assertion reads.
165 SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock"
166 defer_sock "$SOCK35"
167 LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh"
168 defer_rm "$LESSSH"
169 LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt"
170 defer_rm "$LESSDATA"
171 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock"
172 defer_sock "$SOCK36"
173 # The focus-skip leg, on a daemon of its own for the grid-size reason: it
174 # asserts on grid sizes and on how many clients a session ever had at once,
175 # and a session another block created — or another block's client sitting
176 # on this daemon — would make both numbers say nothing.
177 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock"
178 defer_sock "$SOCK37"
179 # The dead tile. Its own daemon because the leg needs a session name that
180 # does NOT exist — the daemon refuses the attach — and any other block's
181 # daemon might have one by that name.
182 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock"
183 defer_sock "$SOCK39"
184 # The wheel inside the focused tile. Two daemons for the wheel pair's own
185 # reason, turned up one notch: each needs a session whose SHELL is fixed for
186 # the session's whole life — one that scrolls and one that ASKS for the
187 # mouse — and a mode another scenario's session set would follow the wall's
188 # terminal around. Both use the daemon's DEFAULT session, so the wall
189 # spelling is a bare `--sock PATH` and nothing has to create anything.
190 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock"
191 defer_sock "$SOCK41"
192 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh"
193 defer_rm "$ZWHEELSH"
194 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
195 defer_sock "$SOCK42"
196 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
197 defer_rm "$ZMOUSESH"
198
199 # ---- the wheel scrolls back, and an app that asks gets it instead -------
200 #
201 # The pair is one claim seen from both sides, so the two legs are the same
202 # session, the same bytes and opposite outcomes: a wheel report belongs to
203 # the scrollback until an application inside the session asks for the mouse,
204 # and then it belongs to that application.
205 #
206 # The bytes are real ones: `\x1b[<64;10;5M` is what a terminal writes for a
207 # wheel-up press once the client has asked for SGR reports (1006), which is
208 # the point — before this the client asked for nothing, so a terminal
209 # answered the wheel by SYNTHESISING arrow keys on the alternate screen
210 # (DEC 1007) and the session read them as input.
211 #
212 # (a) At the live view, eight notches in ONE write scroll a screenful back.
213 # Eight because a notch is three rows: 8*3 = 24, exactly what
214 # Shift+PageUp moves on this 80x24 terminal, which is what lets this
215 # leg borrow tp1's needle — "60" is in the history page (rows 54..77)
216 # and in nothing painted before it. One write rather than eight because
217 # that is how a terminal writes a spin, and because a burst that is not
218 # SUMMED lands three rows up instead of twenty-four: the needle fails.
219 cat > "$WHEELSH" <<'EOF'
220 #!/bin/sh
221 seq 1 100
222 exec /bin/cat
223 EOF
224 chmod +x "$WHEELSH"
225 start_daemon "$SOCK33" "$OUT.whl.d" "wheel daemon never bound" --shell "$WHEELSH"
226 D30PID=$DPID
227 # Attach only once seq has finished, for tp1's reason: the page the wheel
228 # fetches has to be content this client was never sent.
229 i=0
230 until "$MUXD" dump --sock "$SOCK33" | grep -q "100"; do
231 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: wheel session never finished seq"; exit 1; }
232 sleep 0.1
233 done
234 # `\x1b[0m100`, not "100": the client asks its own terminal for mouse
235 # reporting with `\x1b[?1000h`, so a bare "100" is in this capture before
236 # any row is. The SGR reset in front of it is the paint's, and nothing else
237 # writes that pair. (Measured: with the plain needle this leg passes on the
238 # mode enable and sends the wheel before the snapshot has landed.)
239 set +e
240 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.whl" --err "$OUT.whl.err" \
241 -- "$MUX" --sock "$SOCK33" > "$OUT.whl.log" 2>&1 <<'EOF'
242 expect \x1b[0m100 15000
243 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
244 expect 60 15000
245 send \x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M
246 expect \x1b[0m100 15000
247 send \x1b[<64;10;5Mwheel-and-key
248 settle 600 15000
249 send \x1cd
250 waitexit 10000
251 EOF
252 RC=$?
253 set -e
254 [ "$RC" -eq 0 ] || {
255 echo "e2e FAIL: wheel: ptyclient leg exited $RC (did the wheel scroll, and come back?):"
256 cat "$OUT.whl.log"; exit 1; }
257 # The other half of "the wheel is the client's": the bytes are CONSUMED.
258 # Seventeen mouse reports were typed at a shell that would have echoed every
259 # one of them, and the session's grid must hold none.
260 timeout 20 "$MUXA" capture --sock "$SOCK33" > "$OUT.whlcap" 2>&1
261 grep -qF -- "[<64;" "$OUT.whlcap" && {
262 echo "e2e FAIL: wheel: a mouse report reached the pty as input:"
263 cat "$OUT.whlcap"; exit 1; }
264 # ...but a keystroke that shared its read with a notch is NOT consumed. The
265 # last send was one write holding both, typed at the live view: the wheel
266 # moved the view and the letters belong to the shell. Swallowing them as
267 # "the key that leaves scroll mode" loses input to a view the user had not
268 # seen yet, which is what this needle catches.
269 grep -qF -- "wheel-and-key" "$OUT.whlcap" || {
270 echo "e2e FAIL: wheel: a keystroke sharing a read with a notch never reached the pty:"
271 cat "$OUT.whlcap"; exit 1; }
272 assert_stopped "$SOCK33" "$D30PID" "wheel" "$OUT.whlstop"
273 D30PID=""
274 ok "the wheel scrolls back and returns to live, and never reaches the pty"
275
276 # (b) The same session with an application holding the mouse. The script
277 # asks for tracking (1000), drag (1002) and SGR reports (1006) — vim's
278 # `set mouse=a` set — before becoming cat, so every wheel byte is the
279 # application's and the client must hand them over untouched.
280 #
281 # 1003 is the mode that makes this leg sharp, and it is 1003 rather
282 # than 1002 because the client's own capture set is 1000+1002+1006 —
283 # a `?1002h` here would be written whether the session asked or not,
284 # and asserting on it would be asserting nothing. 1003 (report motion
285 # with no button down) is a mode the client never wants for itself, so
286 # a `?1003h` on this terminal can only be the session's, mirrored. The
287 # script asks for it on top of vim's `set mouse=a` set.
288 cat > "$MOUSESH" <<'EOF'
289 #!/bin/sh
290 seq 1 100
291 printf '\033[?1000h\033[?1002h\033[?1003h\033[?1006h'
292 printf 'app-holds-the-mouse\n'
293 exec /bin/cat
294 EOF
295 chmod +x "$MOUSESH"
296 start_daemon "$SOCK34" "$OUT.mse.d" "app-mouse daemon never bound" --shell "$MOUSESH"
297 D31PID=$DPID
298 i=0
299 until "$MUXD" dump --sock "$SOCK34" | grep -q "app-holds-the-mouse"; do
300 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app-mouse session never armed"; exit 1; }
301 sleep 0.1
302 done
303 # The modes are already set when this client attaches, so they arrive in
304 # the attach's own term_modes and there is no race to settle for. The echo
305 # is the assertion: cat is in canonical mode with ECHOCTL, so bytes that
306 # reach the pty come back as `^[[<64;10;5M` and bytes that do not, do not.
307 set +e
308 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.mse" --err "$OUT.mse.err" \
309 -- "$MUX" --sock "$SOCK34" > "$OUT.mse.log" 2>&1 <<'EOF'
310 expect app-holds-the-mouse 15000
311 send \x1b[<64;10;5M
312 expect [<64;10;5M 15000
313 send \x1cd
314 waitexit 10000
315 EOF
316 RC=$?
317 set -e
318 [ "$RC" -eq 0 ] || {
319 echo "e2e FAIL: app mouse: ptyclient leg exited $RC (did the wheel reach the app?):"
320 cat "$OUT.mse.log"; exit 1; }
321 # The session's own grid, not just the client's screen: the echo is the pty
322 # saying it received the bytes.
323 timeout 20 "$MUXA" capture --sock "$SOCK34" > "$OUT.msecap" 2>&1
324 grep -qF -- "[<64;10;5M" "$OUT.msecap" || {
325 echo "e2e FAIL: app mouse: the wheel never reached the application's pty:"
326 cat "$OUT.msecap"; exit 1; }
327 # The mirror: this terminal was asked for the session's modes, not the
328 # client's own set — and 1003 is outside that set, so this cannot pass on
329 # the client's own behalf.
330 grep -qaF "$(printf '\033[?1003h')" "$OUT.mse" || {
331 echo "e2e FAIL: app mouse: the client never mirrored the session's any-motion mode"; exit 1; }
332 # And the negative that makes the pair a pair: this session has the same
333 # 77 rows of history as (a), and the same wheel byte moved none of it.
334 grep -qF -- "60" "$OUT.mse" && {
335 echo "e2e FAIL: app mouse: the client scrolled back on a wheel the app owned"; exit 1; }
336 assert_stopped "$SOCK34" "$D31PID" "app mouse" "$OUT.msestop"
337 D31PID=""
338 ok "an application that asked for the mouse gets the wheel, and the client does not"
339
340 # ---- the wheel on the alternate screen: a pager, not our scrollback ----
341 #
342 # The alt screen has no scrollback of ours — `historyRows` is 0 there by
343 # contract — so a notch that fell through to the scroll arithmetic was
344 # consumed by the mouse filter and then dropped. Measured on this very
345 # scenario before the fix: four notches, grid unmoved.
346 #
347 # `less +G` on 200 lines: the view starts at the END, so a wheel-UP is the
348 # direction with somewhere to go. Eight notches is 24 rows, one screenful,
349 # which moves the top from 178 to 154 — and 178 leaves the screen entirely,
350 # so the assertion has both a needle and its negative.
351 #
352 # LESS is cleared in the script rather than trusted: an operator with
353 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse,
354 # which is the other leg's story, and this one would fail for a reason that
355 # has nothing to do with the code.
356 seq 1 200 > "$LESSDATA"
357 cat > "$LESSSH" <<EOF
358 #!/bin/sh
359 LESS=
360 export LESS
361 exec less +G $LESSDATA
362 EOF
363 chmod +x "$LESSSH"
364 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH"
365 D32PID=$DPID
366 i=0
367 until "$MUXD" dump --sock "$SOCK35" | grep -qx "200"; do
368 i=$((i+1)); [ "$i" -lt 100 ] || {
369 echo "e2e FAIL: pager never reached the end of the file:"
370 "$MUXD" dump --sock "$SOCK35" | tail -3; exit 1; }
371 sleep 0.1
372 done
373 set +e
374 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \
375 -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF'
376 expect 199 15000
377 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
378 settle 800 15000
379 send \x1cd
380 waitexit 10000
381 EOF
382 RC=$?
383 set -e
384 [ "$RC" -eq 0 ] || {
385 echo "e2e FAIL: pager: ptyclient leg exited $RC:"; cat "$OUT.pgr.log"; exit 1; }
386 # `muxd dump`, not `muxa capture`: the assertions below are WHOLE-LINE ones
387 # and capture answers in JSON, where the grid is one line with escaped
388 # newlines in it — a `grep -x` against that can never match. (It did not,
389 # on the first run of this leg, while the grid underneath was exactly
390 # right.) dump prints the grid a row per line, which is what -x needs.
391 "$MUXD" dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1
392 # A whole line, because the pager's status line carries the file's PATH —
393 # which holds this run's pid and could spell any short number.
394 grep -qx "154" "$OUT.pgrcap" || {
395 echo "e2e FAIL: pager: the wheel moved nothing on the alternate screen:"
396 cat "$OUT.pgrcap"; exit 1; }
397 grep -qx "178" "$OUT.pgrcap" && {
398 echo "e2e FAIL: pager: the old top row is still on screen, so the view did not move a screenful:"
399 cat "$OUT.pgrcap"; exit 1; }
400 # The pager never asked for the mouse, so it must never have SEEN a mouse
401 # report: what reached it was arrow keys, which leave no text behind.
402 grep -qF -- "[<64;" "$OUT.pgrcap" && {
403 echo "e2e FAIL: pager: a raw mouse report reached an application that never asked for one:"
404 cat "$OUT.pgrcap"; exit 1; }
405 assert_stopped "$SOCK35" "$D32PID" "pager" "$OUT.pgrstop"
406 D32PID=""
407 ok "the wheel scrolls a pager on the alternate screen, as arrow keys"
408
409 # ---- a client with no terminal filters nothing --------------------------
410 #
411 # The mouse filter is gated on this client having TAKEN a terminal over,
412 # and that gate has to be the alt-screen flag rather than the mode bits:
413 # a client whose stdin is a pipe never asked anyone for mouse reports, so
414 # nothing it reads can be one. Filtering there is pure loss — the bytes are
415 # whatever a script piped in, and an SGR-shaped escape in a heredoc is text
416 # somebody meant to send.
417 #
418 # `cat` as the session, so what arrives is echoed back and the grid IS the
419 # assertion: the bytes reached the pty or they did not.
420 start_daemon "$SOCK36" "$OUT.pipe.d" "pipe-stdin daemon never bound" --shell /bin/cat
421 D33PID=$DPID
422 # The detach chord is a SEPARATE write behind a sleep, and both halves of
423 # that are load-bearing. Without it the client stays attached after stdin
424 # ends — a pipe closing is not a session ending — and the leg would prove
425 # its point by timing out. And it cannot ride the same write as the
426 # payload: a chord ENDS its chunk (PrefixFilter), so bytes ahead of it in
427 # the same read are dropped, which would race the assertion away.
428 { printf 'hello \033[<64;10;5M world\n'; sleep 1; printf '\034d'; } |
429 timeout 20 "$MUX" --sock "$SOCK36" > "$OUT.pipe" 2>&1
430 RC=$?
431 [ "$RC" -eq 0 ] || {
432 echo "e2e FAIL: pipe stdin: the client exited $RC:"; cat "$OUT.pipe"; exit 1; }
433 i=0
434 until timeout 20 "$MUXA" capture --sock "$SOCK36" 2>&1 | grep -qF -- "world"; do
435 i=$((i+1)); [ "$i" -lt 100 ] || {
436 echo "e2e FAIL: pipe stdin: the piped line never reached the pty:"
437 timeout 20 "$MUXA" capture --sock "$SOCK36"; exit 1; }
438 sleep 0.1
439 done
440 timeout 20 "$MUXA" capture --sock "$SOCK36" > "$OUT.pipecap" 2>&1
441 # The whole line, escape included. `cat` echoes control bytes as `^[`, so
442 # what a grid can show of `\x1b[<64;10;5M` is `[<64;10;5M` — and a client
443 # that ate the report leaves `hello world` with the middle missing.
444 grep -qF -- "[<64;10;5M" "$OUT.pipecap" || {
445 echo "e2e FAIL: pipe stdin: an SGR-shaped escape was filtered out of a stream nobody was reporting on:"
446 cat "$OUT.pipecap"; exit 1; }
447 assert_stopped "$SOCK36" "$D33PID" "pipe stdin" "$OUT.pipestop"
448 D33PID=""
449 ok "a client with no terminal of its own forwards SGR-shaped bytes untouched"
450
451 # ---- focus skips between tiles, and the daemon never notices -----------
452 #
453 # Every tile's replica is hot whether or not it is the focus, so moving
454 # the focus between two tiles is a local repaint. No re-dial, no attach,
455 # no snapshot round trip — a claim about what did NOT happen, witnessed
456 # daemon-side rather than by a stopwatch.
457 #
458 # One ptyclient leg, two sessions' worth of typing on two tiles:
459 #
460 # type mark a with zs-one (a is focused from the first byte)
461 # Ctrl-\ n move the focus to tile 2 (session b)
462 # type mark b with zs-two
463 # Ctrl-\ 1 focus tile 1 (session a)
464 # type mark a again with zs-three
465 # Ctrl-\ d leave
466 #
467 # Two claims:
468 #
469 # * every marker landed in the session the focus was on, and in no
470 # other. zs-one and zs-three in a, zs-two in b, and each absent from
471 # the other session — the negatives are what prove the focus MOVED
472 # rather than the wall typing on at one connection.
473 # * the daemon accepted exactly TWO attaches for the whole run — one per
474 # tile, at wall startup. A `Ctrl-\ n` that re-dialled would be three or
475 # more, and the cumulative counter says so however briefly the extra
476 # connection lived. Focus is client-local and sends nothing on the wire.
477 start_daemon "$SOCK37" "$OUT.zs.d" "focus-skip daemon never bound" --shell /bin/sh
478 D34PID=$DPID
479
480 # Both sessions exist before the wall does, each with a marker so the wall
481 # has something to paint and this leg has an anchor to enter on.
482 pipe_mux "$OUT.zsa" "$OUT.zsa.err" timeout 40 "$MUX" --sock "$SOCK37" --session a
483 pipe_send 'printf "zsa-%%s\\n" pin\n'
484 await_out "$OUT.zsa" "zsa-pin" "zsa-pin never reached the client"
485 pipe_detach
486 wait_grid "$SOCK37" "zsa-pin" "focus skip: session a's marker" a
487 pipe_mux "$OUT.zsb" "$OUT.zsb.err" timeout 40 "$MUX" --sock "$SOCK37" --session b
488 pipe_send 'printf "zsb-%%s\\n" pin\n'
489 await_out "$OUT.zsb" "zsb-pin" "zsb-pin never reached the client"
490 pipe_detach
491 wait_grid "$SOCK37" "zsb-pin" "focus skip: session b's marker" b
492
493 ZSATT_BEFORE=$(attaches_now "$SOCK37")
494 watch_clients "$SOCK37" "$OUT.zswatch"
495 set +e
496 # tall: the aspect rule would cut columns at 92x30 (92 >= 60)
497 timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \
498 "$MUX" wall "--sock $SOCK37#a" "--sock $SOCK37#b" > "$OUT.zspc" 2>&1 <<'EOF'
499 expect zsb-pin 20000
500 settle 700 20000
501 send printf 'zs-%s\\n' one\n
502 expect zs-one 15000
503 settle 400 15000
504 send \x1cn
505 settle 700 20000
506 send printf 'zs-%s\\n' two\n
507 expect zs-two 15000
508 settle 400 15000
509 send \x1c1
510 settle 700 20000
511 send printf 'zs-%s\\n' three\n
512 expect zs-three 15000
513 settle 400 15000
514 send \x1cd
515 waitexit 10000
516 EOF
517 RC=$?
518 set -e
519 unwatch_clients "$OUT.zswatch"
520 ZSATT_AFTER=$(attaches_now "$SOCK37")
521 [ "$RC" -eq 0 ] || {
522 echo "e2e FAIL: focus skip: ptyclient leg exited $RC (did \\x1cn and \\x1c1 move the focus?):"
523 cat "$OUT.zspc"; exit 1; }
524 # Where each marker landed. `muxa capture` reads the session's own grid, so
525 # none of this can be an echo of what this script typed at a terminal.
526 timeout 20 "$MUXA" capture --sock "$SOCK37" --session a > "$OUT.zsfa" 2>&1
527 timeout 20 "$MUXA" capture --sock "$SOCK37" --session b > "$OUT.zsfb" 2>&1
528 #
529 # The markers are numbered rather than named after their sessions, and
530 # deliberately so: `zs-b` and `zs-back` were the first spelling, and `zs-b`
531 # matched inside `zs-back` — a negative grep that could never fail, in the
532 # leg whose whole content is negative greps. Numbered words share no
533 # prefix, which is the property this needs.
534 for _m in zs-one zs-three; do
535 grep -q "$_m" "$OUT.zsfa" || {
536 echo "e2e FAIL: focus skip: session a never got $_m:"
537 cat "$OUT.zsfa"; exit 1; }
538 grep -q "$_m" "$OUT.zsfb" && {
539 echo "e2e FAIL: focus skip: $_m reached session b, which the focus had left:"
540 cat "$OUT.zsfb"; exit 1; }
541 done
542 grep -q "zs-two" "$OUT.zsfb" || {
543 echo "e2e FAIL: focus skip: Ctrl-\\ n did not put the focus on session b:"
544 cat "$OUT.zsfb"; exit 1; }
545 grep -q "zs-two" "$OUT.zsfa" && {
546 echo "e2e FAIL: focus skip: zs-two reached session a, so the focus never moved:"
547 cat "$OUT.zsfa"; exit 1; }
548 # The whole point, said by the daemon: two tiles, two attaches, and never a
549 # third across a focus move each way. This is the leg the counter matters
550 # most in — the two moves it makes are exactly the ones a gauge could not
551 # tell from a re-dial, since a connection that closes as another opens
552 # never shows two at once.
553 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip"
554 assert_never_two_clients "$OUT.zswatch" a "focus skip"
555 assert_never_two_clients "$OUT.zswatch" b "focus skip"
556 # Both tiles claimed their rect: 40 cols each (this leg's own width) and 14
557 # rows each (30 terminal rows cut into two stripes of 15, minus one label
558 # bar each). A focus move resizes nothing.
559 for _s in a b; do
560 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1
561 grep -q '"cols":40' "$OUT.zsst$_s" || {
562 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:"
563 cat "$OUT.zsst$_s"; exit 1; }
564 grep -q '"rows":14' "$OUT.zsst$_s" || {
565 echo "e2e FAIL: focus skip: session $_s is not at the height its tile claimed:"
566 cat "$OUT.zsst$_s"; exit 1; }
567 done
568 assert_stopped "$SOCK37" "$D34PID" "focus skip" "$OUT.zsstop"
569 D34PID=""
570 ok "focus skips between tiles with Ctrl-\\ n / Ctrl-\\ l, and the daemon sees no new attach"
571
572 # ---- the wheel inside the focused tile ---------------------------------
573 #
574 # A tile's input goes through the same `interact.Core` a plain client's
575 # does, so the wheel arrives with it: the focused tile scrolls its
576 # session's history on the wheel and never reaches its pty. Same arithmetic
577 # as the plain client's wheel leg (scenario 42) and deliberately the same
578 # pty size, so a difference here is the wall's and not the geometry's:
579 # eight notches in one write are 24 rows, exactly a screenful on 80x24, and
580 # the page that lands holds `wln60` — a line that is in the history and in
581 # nothing painted before it.
582 #
583 # The lines are `wln1`..`wln100` rather than bare numbers because this
584 # terminal is a WALL: its label bar carries the socket path, which carries
585 # this run's pid, and a bare `60` in a pid would make the money assertion
586 # pass for a reason that has nothing to do with the wheel.
587 #
588 # The wall's spelling is a bare `--sock PATH` — the daemon's DEFAULT
589 # session, so nothing has to be created and the session's shell is fixed for
590 # its whole life. One tile, so no label bar: the geometry is the plain
591 # client's.
592 cat > "$ZWHEELSH" <<'EOF'
593 #!/bin/sh
594 seq 1 100 | sed 's/^/wln/'
595 exec /bin/cat
596 EOF
597 chmod +x "$ZWHEELSH"
598 start_daemon "$SOCK41" "$OUT.zw.d" "wheel daemon never bound" --shell "$ZWHEELSH"
599 D38PID=$DPID
600 # The wall attaches only once seq has finished, scenario 42's reason: the
601 # page the wheel fetches has to be content this tile was never sent.
602 i=0
603 until "$MUXD" dump --sock "$SOCK41" | grep -q "wln100"; do
604 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: wheel: session never finished seq"; exit 1; }
605 sleep 0.1
606 done
607 set +e
608 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.zwcap" --err "$OUT.zwcap.err" -- \
609 "$MUX" wall "--sock $SOCK41" > "$OUT.zwpc" 2>&1 <<'EOF'
610 expect wln100 20000
611 settle 700 20000
612 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
613 expect wln60 15000
614 send \x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M
615 expect wln100 15000
616 settle 500 15000
617 send zwheelpin\n
618 expect zwheelpin 15000
619 settle 500 15000
620 send \x1cd
621 waitexit 10000
622 EOF
623 RC=$?
624 set -e
625 [ "$RC" -eq 0 ] || {
626 echo "e2e FAIL: wheel: ptyclient leg exited $RC (did the focused wheel scroll?):"
627 cat "$OUT.zwpc"; exit 1; }
628 # The anchor first, and it is a POSITIVE: a keystroke typed at the focused
629 # tile reached this session's pty. The silent half below is measured against
630 # it, so a leg that fell over early can never pass by having done nothing.
631 timeout 20 "$MUXA" capture --sock "$SOCK41" > "$OUT.zwcapg" 2>&1
632 grep -q "zwheelpin" "$OUT.zwcapg" || {
633 echo "e2e FAIL: wheel: the focus never typed into the session, so the"
634 echo " silent half below proves nothing:"
635 cat "$OUT.zwcapg"; cat "$OUT.zwpc"; exit 1; }
636 # The wheel reports were typed at a shell that echoes everything it is
637 # given, and the session's grid must hold none: the wheel is the client's
638 # own, never forwarded to the pty.
639 grep -qF -- "[<64;" "$OUT.zwcapg" && {
640 echo "e2e FAIL: wheel: a mouse report reached the pty as input:"
641 cat "$OUT.zwcapg"; exit 1; }
642 assert_stopped "$SOCK41" "$D38PID" "wheel" "$OUT.zwstop"
643 D38PID=""
644 ok "the focused tile scrolls its session's history on the wheel, and the pty sees none of it"
645
646 # ---- an application in the focused tile owns the mouse -----------------
647 #
648 # Scenario 43 said this about a plain client; this says it about a tile.
649 # A tile learns the session's modes while it is a STRIPE — `interact.Core`
650 # decodes every `term_modes` whatever the tile's claim is, and only the
651 # WRITE to the terminal is gated — so the handover cannot come from a frame:
652 # it has to come from what the Core already knew, level-set onto the
653 # terminal at the moment of the claim.
654 #
655 # 1003 is what makes the leg sharp, and it has to be 1003: the wall claims
656 # 1000+1002+1006 for itself and a focused tile claims the same set, so
657 # either of those would appear on this terminal with no session involved.
658 # 1003 is in nobody's own set, so a `?1003h` here can only be the session's,
659 # mirrored.
660 cat > "$ZMOUSESH" <<'EOF'
661 #!/bin/sh
662 seq 1 100 | sed 's/^/mln/'
663 printf '\033[?1000h\033[?1002h\033[?1003h\033[?1006h'
664 printf 'mapp-holds-the-mouse\n'
665 exec /bin/cat
666 EOF
667 chmod +x "$ZMOUSESH"
668 start_daemon "$SOCK42" "$OUT.zm2.d" "app-mouse daemon never bound" --shell "$ZMOUSESH"
669 D39PID=$DPID
670 i=0
671 until "$MUXD" dump --sock "$SOCK42" | grep -q "mapp-holds-the-mouse"; do
672 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app mouse: session never armed"; exit 1; }
673 sleep 0.1
674 done
675 # The echo is the assertion: cat is in canonical mode with ECHOCTL, so bytes
676 # that reach the pty come back as `^[[<64;10;5M` and bytes that do not, do
677 # not. The tile claims the terminal on its first pass, so the modes are
678 # level-set before the wheel is sent.
679 set +e
680 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.zm2cap" --err "$OUT.zm2cap.err" -- \
681 "$MUX" wall "--sock $SOCK42" > "$OUT.zm2pc" 2>&1 <<'EOF'
682 expect mapp-holds-the-mouse 20000
683 settle 700 20000
684 send \x1b[<64;10;5M
685 expect [<64;10;5M 15000
686 settle 500 15000
687 send \x1cd
688 waitexit 10000
689 EOF
690 RC=$?
691 set -e
692 [ "$RC" -eq 0 ] || {
693 echo "e2e FAIL: app mouse: ptyclient leg exited $RC (did the wheel reach the app?):"
694 cat "$OUT.zm2pc"; exit 1; }
695 # The session's own grid, not just this terminal: the echo is the pty saying
696 # it received the bytes.
697 timeout 20 "$MUXA" capture --sock "$SOCK42" > "$OUT.zm2capg" 2>&1
698 grep -qF -- "[<64;10;5M" "$OUT.zm2capg" || {
699 echo "e2e FAIL: app mouse: the wheel never reached the application's pty:"
700 cat "$OUT.zm2capg"; exit 1; }
701 # The mirror: this terminal was asked for the SESSION's modes at the claim,
702 # not for the tile's own set — and 1003 is outside that set.
703 grep -qaF "$(printf '\033')[?1003h" "$OUT.zm2cap" || {
704 echo "e2e FAIL: app mouse: the claim never mirrored the session's any-motion mode:"
705 cat "$OUT.zm2pc"; exit 1; }
706 # And the negative that makes the pair a pair: this session has the same 77
707 # rows of history as the leg above, and the same wheel byte moved none of it.
708 grep -qa "mln60" "$OUT.zm2cap" && {
709 echo "e2e FAIL: app mouse: the tile scrolled back on a wheel the app owned:"
710 cat "$OUT.zm2pc"; exit 1; }
711 assert_stopped "$SOCK42" "$D39PID" "app mouse" "$OUT.zm2stop"
712 D39PID=""
713 ok "an application in the focused tile gets the wheel, and the tile does not"
714
715 # ---- a tile whose pump has died still says something -------------------
716 #
717 # A dead tile keeps its rect and narrates on its label bar — the only
718 # paint a pump with nobody left to answer `repaint_gen` ever does. The
719 # keyboard is the only thread left, and it draws that bar so the wall
720 # never silently loses a row.
721 #
722 # The tile here is dead by the most ordinary route: a spelling naming a
723 # session that does not exist. The daemon refuses the attach and that pump
724 # ends — `[refused]`, the state the wall already had a word for. Its bar
725 # says so from the first paint, and the live tile beside it is unharmed:
726 # the wall is still a wall, and the focus stays on the tile that can be
727 # typed at.
728 #
729 # Deliberately NOT asserted: that a `[reconnecting]` tile gets the same
730 # treatment. It must not — it still has a thread that will repaint its hot
731 # replica within a poll timeout, and the keyboard drawing over that would
732 # replace something true with something stale.
733 start_daemon "$SOCK39" "$OUT.zd.d" "dead-tile daemon never bound" --shell /bin/sh
734 D36PID=$DPID
735 pipe_mux "$OUT.zda" "$OUT.zda.err" timeout 40 "$MUX" --sock "$SOCK39" --session a
736 pipe_send 'printf "zdlive-%%s\\n" pin\n'
737 await_out "$OUT.zda" "zdlive-pin" "zdlive-pin never reached the client"
738 pipe_detach
739 wait_grid "$SOCK39" "zdlive-pin" "dead tile: the live session's marker" a
740
741 set +e
742 # tall: two tiles at 90x24 trip the aspect rule (90 >= 48); 70x36 stays
743 # stacked (70 < 72) and fits the bar label the assertion greps for
744 timeout 90 "$PTYCLIENT" --cols 70 --rows 36 --out "$OUT.zdcap" --err "$OUT.zdcap.err" -- \
745 "$MUX" wall "--sock $SOCK39#a" "--sock $SOCK39#ghost" > "$OUT.zdpc" 2>&1 <<'EOF'
746 expect zdlive-pin 20000
747 settle 800 20000
748 send printf 'zdagain-%s\\n' pin\n
749 expect zdagain-pin 15000
750 settle 400 15000
751 send \x1c2
752 settle 400 15000
753 send \x1c:abc
754 settle 400 15000
755 send \x1b
756 settle 400 15000
757 send \x1c:zz#bad name
758 settle 400 15000
759 send \r
760 expect [bad target 10000
761 settle 600 15000
762 send \x1c1
763 settle 400 15000
764 send \x1cd
765 waitexit 10000
766 EOF
767 RC=$?
768 set -e
769 [ "$RC" -eq 0 ] || {
770 echo "e2e FAIL: dead tile: ptyclient leg exited $RC:"
771 cat "$OUT.zdpc"; exit 1; }
772 # The dead tile narrated its own refusal: the state word the bar can only
773 # have if the pump reached the refusal path. Painted from the first
774 # Painted from the first relayout, and never cleared.
775 grep -q -- "--sock $SOCK39#ghost \[refused\]" "$OUT.zdcap" || {
776 echo "e2e FAIL: dead tile: the dead tile's bar never said [refused]"; exit 1; }
777 # The live tile is unharmed and answering — the wall is still a wall.
778 grep -q -- "--sock $SOCK39#a \[up\]" "$OUT.zdcap" || {
779 echo "e2e FAIL: dead tile: the live tile's bar never said [up]"; exit 1; }
780 # The focus stayed on the live tile: the marker typed after the wall came
781 # up landed in a, which is the tile that can be typed at. A focus that
782 # landed on the dead one would have dropped these bytes.
783 timeout 20 "$MUXA" capture --sock "$SOCK39" --session a > "$OUT.zdcapa" 2>&1
784 grep -q "zdagain-pin" "$OUT.zdcapa" || {
785 echo "e2e FAIL: dead tile: the live session never got the focused marker:"
786 cat "$OUT.zdcapa"; exit 1; }
787 # `Ctrl-\ :` is the one chord that works with the focus on a dead tile —
788 # the splits go through the pump's `ask`, and a dead pump answers nothing —
789 # so it is the one chord that can leave a line on a row nothing repaints.
790 # The oracle, not the byte stream: a banner LEFT on the screen is in the
791 # capture either way, and only the render says what the human was still
792 # looking at.
793 "$RENDER" --cols 70 --rows 36 < "$OUT.zdcap" > "$OUT.zdgrid" || {
794 echo "e2e FAIL: dead tile: render oracle failed"
795 cat "$OUT.zdgrid"; exit 1; }
796 # The refusal reached the eyes that earned it. Nothing claims a dead tile,
797 # so a notice only the claim path can show would never have been seen.
798 grep -q "\[bad target" "$OUT.zdgrid" || {
799 echo "e2e FAIL: dead tile: a refused prompt never narrated on the dead tile:"
800 cat "$OUT.zdgrid"; exit 1; }
801 grep -q ": zz#bad" "$OUT.zdgrid" && {
802 echo "e2e FAIL: dead tile: the refused prompt is still on the dead tile's row:"
803 cat "$OUT.zdgrid"; exit 1; }
804 # And Esc leaves nothing behind either: same row, same missing claim.
805 grep -q ": abc" "$OUT.zdgrid" && {
806 echo "e2e FAIL: dead tile: the cancelled prompt is still on the dead tile's row:"
807 cat "$OUT.zdgrid"; exit 1; }
808 assert_stopped "$SOCK39" "$D36PID" "dead tile" "$OUT.zdstop"
809 D36PID=""
810 ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one"
811
812 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its 148 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
813 # own, for the dynamic-wall leg's reason turned up one notch: what these 149 # own, for the dynamic-wall leg's reason turned up one notch: what these
814 # two blocks read back is the wall FILE, and every `mux` in this suite now 150 # two blocks read back is the wall FILE, and every `mux` in this suite now
test/e2e_08_mouse.sh
Old New
@@ -0,0 +1,667 @@
1 # shellcheck shell=sh
2 # e2e_08_mouse.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in
3 # the order they stand in; see the lib's header for what this file may
4 # assume and what it must register.
5 # The wheel pair. Each needs a scrollback of its own to scroll (or to prove
6 # it did not), which the long-lived /bin/sh daemon has no way to hold still,
7 # and the second one needs a session that ASKS for the mouse — a mode that
8 # would follow every other scenario sharing the daemon around.
9 SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock"
10 defer_sock "$SOCK33"
11 WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh"
12 defer_rm "$WHEELSH"
13 SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock"
14 defer_sock "$SOCK34"
15 MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh"
16 defer_rm "$MOUSESH"
17 # The alternate-screen half of the wheel: its own daemon because the
18 # session under test is a PAGER holding the alt screen for its whole life,
19 # which no other scenario could share a grid with. And the no-terminal
20 # half, whose session is a bare `cat` so that what reaches the pty is
21 # echoed back into the grid the assertion reads.
22 SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock"
23 defer_sock "$SOCK35"
24 LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh"
25 defer_rm "$LESSSH"
26 LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt"
27 defer_rm "$LESSDATA"
28 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock"
29 defer_sock "$SOCK36"
30 # The focus-skip leg, on a daemon of its own for the grid-size reason: it
31 # asserts on grid sizes and on how many clients a session ever had at once,
32 # and a session another block created — or another block's client sitting
33 # on this daemon — would make both numbers say nothing.
34 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock"
35 defer_sock "$SOCK37"
36 # The dead tile. Its own daemon because the leg needs a session name that
37 # does NOT exist — the daemon refuses the attach — and any other block's
38 # daemon might have one by that name.
39 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock"
40 defer_sock "$SOCK39"
41 # The wheel inside the focused tile. Two daemons for the wheel pair's own
42 # reason, turned up one notch: each needs a session whose SHELL is fixed for
43 # the session's whole life — one that scrolls and one that ASKS for the
44 # mouse — and a mode another scenario's session set would follow the wall's
45 # terminal around. Both use the daemon's DEFAULT session, so the wall
46 # spelling is a bare `--sock PATH` and nothing has to create anything.
47 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock"
48 defer_sock "$SOCK41"
49 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh"
50 defer_rm "$ZWHEELSH"
51 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
52 defer_sock "$SOCK42"
53 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
54 defer_rm "$ZMOUSESH"
55
56 # ---- the wheel scrolls back, and an app that asks gets it instead -------
57 #
58 # The pair is one claim seen from both sides, so the two legs are the same
59 # session, the same bytes and opposite outcomes: a wheel report belongs to
60 # the scrollback until an application inside the session asks for the mouse,
61 # and then it belongs to that application.
62 #
63 # The bytes are real ones: `\x1b[<64;10;5M` is what a terminal writes for a
64 # wheel-up press once the client has asked for SGR reports (1006), which is
65 # the point — before this the client asked for nothing, so a terminal
66 # answered the wheel by SYNTHESISING arrow keys on the alternate screen
67 # (DEC 1007) and the session read them as input.
68 #
69 # (a) At the live view, eight notches in ONE write scroll a screenful back.
70 # Eight because a notch is three rows: 8*3 = 24, exactly what
71 # Shift+PageUp moves on this 80x24 terminal, which is what lets this
72 # leg borrow tp1's needle — "60" is in the history page (rows 54..77)
73 # and in nothing painted before it. One write rather than eight because
74 # that is how a terminal writes a spin, and because a burst that is not
75 # SUMMED lands three rows up instead of twenty-four: the needle fails.
76 cat > "$WHEELSH" <<'EOF'
77 #!/bin/sh
78 seq 1 100
79 exec /bin/cat
80 EOF
81 chmod +x "$WHEELSH"
82 start_daemon "$SOCK33" "$OUT.whl.d" "wheel daemon never bound" --shell "$WHEELSH"
83 D30PID=$DPID
84 # Attach only once seq has finished, for tp1's reason: the page the wheel
85 # fetches has to be content this client was never sent.
86 i=0
87 until "$MUXD" dump --sock "$SOCK33" | grep -q "100"; do
88 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: wheel session never finished seq"; exit 1; }
89 sleep 0.1
90 done
91 # `\x1b[0m100`, not "100": the client asks its own terminal for mouse
92 # reporting with `\x1b[?1000h`, so a bare "100" is in this capture before
93 # any row is. The SGR reset in front of it is the paint's, and nothing else
94 # writes that pair. (Measured: with the plain needle this leg passes on the
95 # mode enable and sends the wheel before the snapshot has landed.)
96 set +e
97 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.whl" --err "$OUT.whl.err" \
98 -- "$MUX" --sock "$SOCK33" > "$OUT.whl.log" 2>&1 <<'EOF'
99 expect \x1b[0m100 15000
100 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
101 expect 60 15000
102 send \x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M
103 expect \x1b[0m100 15000
104 send \x1b[<64;10;5Mwheel-and-key
105 settle 600 15000
106 send \x1cd
107 waitexit 10000
108 EOF
109 RC=$?
110 set -e
111 [ "$RC" -eq 0 ] || {
112 echo "e2e FAIL: wheel: ptyclient leg exited $RC (did the wheel scroll, and come back?):"
113 cat "$OUT.whl.log"; exit 1; }
114 # The other half of "the wheel is the client's": the bytes are CONSUMED.
115 # Seventeen mouse reports were typed at a shell that would have echoed every
116 # one of them, and the session's grid must hold none.
117 timeout 20 "$MUXA" capture --sock "$SOCK33" > "$OUT.whlcap" 2>&1
118 grep -qF -- "[<64;" "$OUT.whlcap" && {
119 echo "e2e FAIL: wheel: a mouse report reached the pty as input:"
120 cat "$OUT.whlcap"; exit 1; }
121 # ...but a keystroke that shared its read with a notch is NOT consumed. The
122 # last send was one write holding both, typed at the live view: the wheel
123 # moved the view and the letters belong to the shell. Swallowing them as
124 # "the key that leaves scroll mode" loses input to a view the user had not
125 # seen yet, which is what this needle catches.
126 grep -qF -- "wheel-and-key" "$OUT.whlcap" || {
127 echo "e2e FAIL: wheel: a keystroke sharing a read with a notch never reached the pty:"
128 cat "$OUT.whlcap"; exit 1; }
129 assert_stopped "$SOCK33" "$D30PID" "wheel" "$OUT.whlstop"
130 D30PID=""
131 ok "the wheel scrolls back and returns to live, and never reaches the pty"
132
133 # (b) The same session with an application holding the mouse. The script
134 # asks for tracking (1000), drag (1002) and SGR reports (1006) — vim's
135 # `set mouse=a` set — before becoming cat, so every wheel byte is the
136 # application's and the client must hand them over untouched.
137 #
138 # 1003 is the mode that makes this leg sharp, and it is 1003 rather
139 # than 1002 because the client's own capture set is 1000+1002+1006 —
140 # a `?1002h` here would be written whether the session asked or not,
141 # and asserting on it would be asserting nothing. 1003 (report motion
142 # with no button down) is a mode the client never wants for itself, so
143 # a `?1003h` on this terminal can only be the session's, mirrored. The
144 # script asks for it on top of vim's `set mouse=a` set.
145 cat > "$MOUSESH" <<'EOF'
146 #!/bin/sh
147 seq 1 100
148 printf '\033[?1000h\033[?1002h\033[?1003h\033[?1006h'
149 printf 'app-holds-the-mouse\n'
150 exec /bin/cat
151 EOF
152 chmod +x "$MOUSESH"
153 start_daemon "$SOCK34" "$OUT.mse.d" "app-mouse daemon never bound" --shell "$MOUSESH"
154 D31PID=$DPID
155 i=0
156 until "$MUXD" dump --sock "$SOCK34" | grep -q "app-holds-the-mouse"; do
157 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app-mouse session never armed"; exit 1; }
158 sleep 0.1
159 done
160 # The modes are already set when this client attaches, so they arrive in
161 # the attach's own term_modes and there is no race to settle for. The echo
162 # is the assertion: cat is in canonical mode with ECHOCTL, so bytes that
163 # reach the pty come back as `^[[<64;10;5M` and bytes that do not, do not.
164 set +e
165 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.mse" --err "$OUT.mse.err" \
166 -- "$MUX" --sock "$SOCK34" > "$OUT.mse.log" 2>&1 <<'EOF'
167 expect app-holds-the-mouse 15000
168 send \x1b[<64;10;5M
169 expect [<64;10;5M 15000
170 send \x1cd
171 waitexit 10000
172 EOF
173 RC=$?
174 set -e
175 [ "$RC" -eq 0 ] || {
176 echo "e2e FAIL: app mouse: ptyclient leg exited $RC (did the wheel reach the app?):"
177 cat "$OUT.mse.log"; exit 1; }
178 # The session's own grid, not just the client's screen: the echo is the pty
179 # saying it received the bytes.
180 timeout 20 "$MUXA" capture --sock "$SOCK34" > "$OUT.msecap" 2>&1
181 grep -qF -- "[<64;10;5M" "$OUT.msecap" || {
182 echo "e2e FAIL: app mouse: the wheel never reached the application's pty:"
183 cat "$OUT.msecap"; exit 1; }
184 # The mirror: this terminal was asked for the session's modes, not the
185 # client's own set — and 1003 is outside that set, so this cannot pass on
186 # the client's own behalf.
187 grep -qaF "$(printf '\033[?1003h')" "$OUT.mse" || {
188 echo "e2e FAIL: app mouse: the client never mirrored the session's any-motion mode"; exit 1; }
189 # And the negative that makes the pair a pair: this session has the same
190 # 77 rows of history as (a), and the same wheel byte moved none of it.
191 grep -qF -- "60" "$OUT.mse" && {
192 echo "e2e FAIL: app mouse: the client scrolled back on a wheel the app owned"; exit 1; }
193 assert_stopped "$SOCK34" "$D31PID" "app mouse" "$OUT.msestop"
194 D31PID=""
195 ok "an application that asked for the mouse gets the wheel, and the client does not"
196
197 # ---- the wheel on the alternate screen: a pager, not our scrollback ----
198 #
199 # The alt screen has no scrollback of ours — `historyRows` is 0 there by
200 # contract — so a notch that fell through to the scroll arithmetic was
201 # consumed by the mouse filter and then dropped. Measured on this very
202 # scenario before the fix: four notches, grid unmoved.
203 #
204 # `less +G` on 200 lines: the view starts at the END, so a wheel-UP is the
205 # direction with somewhere to go. Eight notches is 24 rows, one screenful,
206 # which moves the top from 178 to 154 — and 178 leaves the screen entirely,
207 # so the assertion has both a needle and its negative.
208 #
209 # LESS is cleared in the script rather than trusted: an operator with
210 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse,
211 # which is the other leg's story, and this one would fail for a reason that
212 # has nothing to do with the code.
213 seq 1 200 > "$LESSDATA"
214 cat > "$LESSSH" <<EOF
215 #!/bin/sh
216 LESS=
217 export LESS
218 exec less +G $LESSDATA
219 EOF
220 chmod +x "$LESSSH"
221 start_daemon "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" --shell "$LESSSH"
222 D32PID=$DPID
223 i=0
224 until "$MUXD" dump --sock "$SOCK35" | grep -qx "200"; do
225 i=$((i+1)); [ "$i" -lt 100 ] || {
226 echo "e2e FAIL: pager never reached the end of the file:"
227 "$MUXD" dump --sock "$SOCK35" | tail -3; exit 1; }
228 sleep 0.1
229 done
230 set +e
231 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \
232 -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF'
233 expect 199 15000
234 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
235 settle 800 15000
236 send \x1cd
237 waitexit 10000
238 EOF
239 RC=$?
240 set -e
241 [ "$RC" -eq 0 ] || {
242 echo "e2e FAIL: pager: ptyclient leg exited $RC:"; cat "$OUT.pgr.log"; exit 1; }
243 # `muxd dump`, not `muxa capture`: the assertions below are WHOLE-LINE ones
244 # and capture answers in JSON, where the grid is one line with escaped
245 # newlines in it — a `grep -x` against that can never match. (It did not,
246 # on the first run of this leg, while the grid underneath was exactly
247 # right.) dump prints the grid a row per line, which is what -x needs.
248 "$MUXD" dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1
249 # A whole line, because the pager's status line carries the file's PATH —
250 # which holds this run's pid and could spell any short number.
251 grep -qx "154" "$OUT.pgrcap" || {
252 echo "e2e FAIL: pager: the wheel moved nothing on the alternate screen:"
253 cat "$OUT.pgrcap"; exit 1; }
254 grep -qx "178" "$OUT.pgrcap" && {
255 echo "e2e FAIL: pager: the old top row is still on screen, so the view did not move a screenful:"
256 cat "$OUT.pgrcap"; exit 1; }
257 # The pager never asked for the mouse, so it must never have SEEN a mouse
258 # report: what reached it was arrow keys, which leave no text behind.
259 grep -qF -- "[<64;" "$OUT.pgrcap" && {
260 echo "e2e FAIL: pager: a raw mouse report reached an application that never asked for one:"
261 cat "$OUT.pgrcap"; exit 1; }
262 assert_stopped "$SOCK35" "$D32PID" "pager" "$OUT.pgrstop"
263 D32PID=""
264 ok "the wheel scrolls a pager on the alternate screen, as arrow keys"
265
266 # ---- a client with no terminal filters nothing --------------------------
267 #
268 # The mouse filter is gated on this client having TAKEN a terminal over,
269 # and that gate has to be the alt-screen flag rather than the mode bits:
270 # a client whose stdin is a pipe never asked anyone for mouse reports, so
271 # nothing it reads can be one. Filtering there is pure loss — the bytes are
272 # whatever a script piped in, and an SGR-shaped escape in a heredoc is text
273 # somebody meant to send.
274 #
275 # `cat` as the session, so what arrives is echoed back and the grid IS the
276 # assertion: the bytes reached the pty or they did not.
277 start_daemon "$SOCK36" "$OUT.pipe.d" "pipe-stdin daemon never bound" --shell /bin/cat
278 D33PID=$DPID
279 # The detach chord is a SEPARATE write behind a sleep, and both halves of
280 # that are load-bearing. Without it the client stays attached after stdin
281 # ends — a pipe closing is not a session ending — and the leg would prove
282 # its point by timing out. And it cannot ride the same write as the
283 # payload: a chord ENDS its chunk (PrefixFilter), so bytes ahead of it in
284 # the same read are dropped, which would race the assertion away.
285 { printf 'hello \033[<64;10;5M world\n'; sleep 1; printf '\034d'; } |
286 timeout 20 "$MUX" --sock "$SOCK36" > "$OUT.pipe" 2>&1
287 RC=$?
288 [ "$RC" -eq 0 ] || {
289 echo "e2e FAIL: pipe stdin: the client exited $RC:"; cat "$OUT.pipe"; exit 1; }
290 i=0
291 until timeout 20 "$MUXA" capture --sock "$SOCK36" 2>&1 | grep -qF -- "world"; do
292 i=$((i+1)); [ "$i" -lt 100 ] || {
293 echo "e2e FAIL: pipe stdin: the piped line never reached the pty:"
294 timeout 20 "$MUXA" capture --sock "$SOCK36"; exit 1; }
295 sleep 0.1
296 done
297 timeout 20 "$MUXA" capture --sock "$SOCK36" > "$OUT.pipecap" 2>&1
298 # The whole line, escape included. `cat` echoes control bytes as `^[`, so
299 # what a grid can show of `\x1b[<64;10;5M` is `[<64;10;5M` — and a client
300 # that ate the report leaves `hello world` with the middle missing.
301 grep -qF -- "[<64;10;5M" "$OUT.pipecap" || {
302 echo "e2e FAIL: pipe stdin: an SGR-shaped escape was filtered out of a stream nobody was reporting on:"
303 cat "$OUT.pipecap"; exit 1; }
304 assert_stopped "$SOCK36" "$D33PID" "pipe stdin" "$OUT.pipestop"
305 D33PID=""
306 ok "a client with no terminal of its own forwards SGR-shaped bytes untouched"
307
308 # ---- focus skips between tiles, and the daemon never notices -----------
309 #
310 # Every tile's replica is hot whether or not it is the focus, so moving
311 # the focus between two tiles is a local repaint. No re-dial, no attach,
312 # no snapshot round trip — a claim about what did NOT happen, witnessed
313 # daemon-side rather than by a stopwatch.
314 #
315 # One ptyclient leg, two sessions' worth of typing on two tiles:
316 #
317 # type mark a with zs-one (a is focused from the first byte)
318 # Ctrl-\ n move the focus to tile 2 (session b)
319 # type mark b with zs-two
320 # Ctrl-\ 1 focus tile 1 (session a)
321 # type mark a again with zs-three
322 # Ctrl-\ d leave
323 #
324 # Two claims:
325 #
326 # * every marker landed in the session the focus was on, and in no
327 # other. zs-one and zs-three in a, zs-two in b, and each absent from
328 # the other session — the negatives are what prove the focus MOVED
329 # rather than the wall typing on at one connection.
330 # * the daemon accepted exactly TWO attaches for the whole run — one per
331 # tile, at wall startup. A `Ctrl-\ n` that re-dialled would be three or
332 # more, and the cumulative counter says so however briefly the extra
333 # connection lived. Focus is client-local and sends nothing on the wire.
334 start_daemon "$SOCK37" "$OUT.zs.d" "focus-skip daemon never bound" --shell /bin/sh
335 D34PID=$DPID
336
337 # Both sessions exist before the wall does, each with a marker so the wall
338 # has something to paint and this leg has an anchor to enter on.
339 pipe_mux "$OUT.zsa" "$OUT.zsa.err" timeout 40 "$MUX" --sock "$SOCK37" --session a
340 pipe_send 'printf "zsa-%%s\\n" pin\n'
341 await_out "$OUT.zsa" "zsa-pin" "zsa-pin never reached the client"
342 pipe_detach
343 wait_grid "$SOCK37" "zsa-pin" "focus skip: session a's marker" a
344 pipe_mux "$OUT.zsb" "$OUT.zsb.err" timeout 40 "$MUX" --sock "$SOCK37" --session b
345 pipe_send 'printf "zsb-%%s\\n" pin\n'
346 await_out "$OUT.zsb" "zsb-pin" "zsb-pin never reached the client"
347 pipe_detach
348 wait_grid "$SOCK37" "zsb-pin" "focus skip: session b's marker" b
349
350 ZSATT_BEFORE=$(attaches_now "$SOCK37")
351 watch_clients "$SOCK37" "$OUT.zswatch"
352 set +e
353 # tall: the aspect rule would cut columns at 92x30 (92 >= 60)
354 timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \
355 "$MUX" wall "--sock $SOCK37#a" "--sock $SOCK37#b" > "$OUT.zspc" 2>&1 <<'EOF'
356 expect zsb-pin 20000
357 settle 700 20000
358 send printf 'zs-%s\\n' one\n
359 expect zs-one 15000
360 settle 400 15000
361 send \x1cn
362 settle 700 20000
363 send printf 'zs-%s\\n' two\n
364 expect zs-two 15000
365 settle 400 15000
366 send \x1c1
367 settle 700 20000
368 send printf 'zs-%s\\n' three\n
369 expect zs-three 15000
370 settle 400 15000
371 send \x1cd
372 waitexit 10000
373 EOF
374 RC=$?
375 set -e
376 unwatch_clients "$OUT.zswatch"
377 ZSATT_AFTER=$(attaches_now "$SOCK37")
378 [ "$RC" -eq 0 ] || {
379 echo "e2e FAIL: focus skip: ptyclient leg exited $RC (did \\x1cn and \\x1c1 move the focus?):"
380 cat "$OUT.zspc"; exit 1; }
381 # Where each marker landed. `muxa capture` reads the session's own grid, so
382 # none of this can be an echo of what this script typed at a terminal.
383 timeout 20 "$MUXA" capture --sock "$SOCK37" --session a > "$OUT.zsfa" 2>&1
384 timeout 20 "$MUXA" capture --sock "$SOCK37" --session b > "$OUT.zsfb" 2>&1
385 #
386 # The markers are numbered rather than named after their sessions, and
387 # deliberately so: `zs-b` and `zs-back` were the first spelling, and `zs-b`
388 # matched inside `zs-back` — a negative grep that could never fail, in the
389 # leg whose whole content is negative greps. Numbered words share no
390 # prefix, which is the property this needs.
391 for _m in zs-one zs-three; do
392 grep -q "$_m" "$OUT.zsfa" || {
393 echo "e2e FAIL: focus skip: session a never got $_m:"
394 cat "$OUT.zsfa"; exit 1; }
395 grep -q "$_m" "$OUT.zsfb" && {
396 echo "e2e FAIL: focus skip: $_m reached session b, which the focus had left:"
397 cat "$OUT.zsfb"; exit 1; }
398 done
399 grep -q "zs-two" "$OUT.zsfb" || {
400 echo "e2e FAIL: focus skip: Ctrl-\\ n did not put the focus on session b:"
401 cat "$OUT.zsfb"; exit 1; }
402 grep -q "zs-two" "$OUT.zsfa" && {
403 echo "e2e FAIL: focus skip: zs-two reached session a, so the focus never moved:"
404 cat "$OUT.zsfa"; exit 1; }
405 # The whole point, said by the daemon: two tiles, two attaches, and never a
406 # third across a focus move each way. This is the leg the counter matters
407 # most in — the two moves it makes are exactly the ones a gauge could not
408 # tell from a re-dial, since a connection that closes as another opens
409 # never shows two at once.
410 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip"
411 assert_never_two_clients "$OUT.zswatch" a "focus skip"
412 assert_never_two_clients "$OUT.zswatch" b "focus skip"
413 # Both tiles claimed their rect: 40 cols each (this leg's own width) and 14
414 # rows each (30 terminal rows cut into two stripes of 15, minus one label
415 # bar each). A focus move resizes nothing.
416 for _s in a b; do
417 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1
418 grep -q '"cols":40' "$OUT.zsst$_s" || {
419 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:"
420 cat "$OUT.zsst$_s"; exit 1; }
421 grep -q '"rows":14' "$OUT.zsst$_s" || {
422 echo "e2e FAIL: focus skip: session $_s is not at the height its tile claimed:"
423 cat "$OUT.zsst$_s"; exit 1; }
424 done
425 assert_stopped "$SOCK37" "$D34PID" "focus skip" "$OUT.zsstop"
426 D34PID=""
427 ok "focus skips between tiles with Ctrl-\\ n / Ctrl-\\ l, and the daemon sees no new attach"
428
429 # ---- the wheel inside the focused tile ---------------------------------
430 #
431 # A tile's input goes through the same `interact.Core` a plain client's
432 # does, so the wheel arrives with it: the focused tile scrolls its
433 # session's history on the wheel and never reaches its pty. Same arithmetic
434 # as the plain client's wheel leg (scenario 42) and deliberately the same
435 # pty size, so a difference here is the wall's and not the geometry's:
436 # eight notches in one write are 24 rows, exactly a screenful on 80x24, and
437 # the page that lands holds `wln60` — a line that is in the history and in
438 # nothing painted before it.
439 #
440 # The lines are `wln1`..`wln100` rather than bare numbers because this
441 # terminal is a WALL: its label bar carries the socket path, which carries
442 # this run's pid, and a bare `60` in a pid would make the money assertion
443 # pass for a reason that has nothing to do with the wheel.
444 #
445 # The wall's spelling is a bare `--sock PATH` — the daemon's DEFAULT
446 # session, so nothing has to be created and the session's shell is fixed for
447 # its whole life. One tile, so no label bar: the geometry is the plain
448 # client's.
449 cat > "$ZWHEELSH" <<'EOF'
450 #!/bin/sh
451 seq 1 100 | sed 's/^/wln/'
452 exec /bin/cat
453 EOF
454 chmod +x "$ZWHEELSH"
455 start_daemon "$SOCK41" "$OUT.zw.d" "wheel daemon never bound" --shell "$ZWHEELSH"
456 D38PID=$DPID
457 # The wall attaches only once seq has finished, scenario 42's reason: the
458 # page the wheel fetches has to be content this tile was never sent.
459 i=0
460 until "$MUXD" dump --sock "$SOCK41" | grep -q "wln100"; do
461 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: wheel: session never finished seq"; exit 1; }
462 sleep 0.1
463 done
464 set +e
465 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.zwcap" --err "$OUT.zwcap.err" -- \
466 "$MUX" wall "--sock $SOCK41" > "$OUT.zwpc" 2>&1 <<'EOF'
467 expect wln100 20000
468 settle 700 20000
469 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
470 expect wln60 15000
471 send \x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M\x1b[<65;10;5M
472 expect wln100 15000
473 settle 500 15000
474 send zwheelpin\n
475 expect zwheelpin 15000
476 settle 500 15000
477 send \x1cd
478 waitexit 10000
479 EOF
480 RC=$?
481 set -e
482 [ "$RC" -eq 0 ] || {
483 echo "e2e FAIL: wheel: ptyclient leg exited $RC (did the focused wheel scroll?):"
484 cat "$OUT.zwpc"; exit 1; }
485 # The anchor first, and it is a POSITIVE: a keystroke typed at the focused
486 # tile reached this session's pty. The silent half below is measured against
487 # it, so a leg that fell over early can never pass by having done nothing.
488 timeout 20 "$MUXA" capture --sock "$SOCK41" > "$OUT.zwcapg" 2>&1
489 grep -q "zwheelpin" "$OUT.zwcapg" || {
490 echo "e2e FAIL: wheel: the focus never typed into the session, so the"
491 echo " silent half below proves nothing:"
492 cat "$OUT.zwcapg"; cat "$OUT.zwpc"; exit 1; }
493 # The wheel reports were typed at a shell that echoes everything it is
494 # given, and the session's grid must hold none: the wheel is the client's
495 # own, never forwarded to the pty.
496 grep -qF -- "[<64;" "$OUT.zwcapg" && {
497 echo "e2e FAIL: wheel: a mouse report reached the pty as input:"
498 cat "$OUT.zwcapg"; exit 1; }
499 assert_stopped "$SOCK41" "$D38PID" "wheel" "$OUT.zwstop"
500 D38PID=""
501 ok "the focused tile scrolls its session's history on the wheel, and the pty sees none of it"
502
503 # ---- an application in the focused tile owns the mouse -----------------
504 #
505 # Scenario 43 said this about a plain client; this says it about a tile.
506 # A tile learns the session's modes while it is a STRIPE — `interact.Core`
507 # decodes every `term_modes` whatever the tile's claim is, and only the
508 # WRITE to the terminal is gated — so the handover cannot come from a frame:
509 # it has to come from what the Core already knew, level-set onto the
510 # terminal at the moment of the claim.
511 #
512 # 1003 is what makes the leg sharp, and it has to be 1003: the wall claims
513 # 1000+1002+1006 for itself and a focused tile claims the same set, so
514 # either of those would appear on this terminal with no session involved.
515 # 1003 is in nobody's own set, so a `?1003h` here can only be the session's,
516 # mirrored.
517 cat > "$ZMOUSESH" <<'EOF'
518 #!/bin/sh
519 seq 1 100 | sed 's/^/mln/'
520 printf '\033[?1000h\033[?1002h\033[?1003h\033[?1006h'
521 printf 'mapp-holds-the-mouse\n'
522 exec /bin/cat
523 EOF
524 chmod +x "$ZMOUSESH"
525 start_daemon "$SOCK42" "$OUT.zm2.d" "app-mouse daemon never bound" --shell "$ZMOUSESH"
526 D39PID=$DPID
527 i=0
528 until "$MUXD" dump --sock "$SOCK42" | grep -q "mapp-holds-the-mouse"; do
529 i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app mouse: session never armed"; exit 1; }
530 sleep 0.1
531 done
532 # The echo is the assertion: cat is in canonical mode with ECHOCTL, so bytes
533 # that reach the pty come back as `^[[<64;10;5M` and bytes that do not, do
534 # not. The tile claims the terminal on its first pass, so the modes are
535 # level-set before the wheel is sent.
536 set +e
537 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.zm2cap" --err "$OUT.zm2cap.err" -- \
538 "$MUX" wall "--sock $SOCK42" > "$OUT.zm2pc" 2>&1 <<'EOF'
539 expect mapp-holds-the-mouse 20000
540 settle 700 20000
541 send \x1b[<64;10;5M
542 expect [<64;10;5M 15000
543 settle 500 15000
544 send \x1cd
545 waitexit 10000
546 EOF
547 RC=$?
548 set -e
549 [ "$RC" -eq 0 ] || {
550 echo "e2e FAIL: app mouse: ptyclient leg exited $RC (did the wheel reach the app?):"
551 cat "$OUT.zm2pc"; exit 1; }
552 # The session's own grid, not just this terminal: the echo is the pty saying
553 # it received the bytes.
554 timeout 20 "$MUXA" capture --sock "$SOCK42" > "$OUT.zm2capg" 2>&1
555 grep -qF -- "[<64;10;5M" "$OUT.zm2capg" || {
556 echo "e2e FAIL: app mouse: the wheel never reached the application's pty:"
557 cat "$OUT.zm2capg"; exit 1; }
558 # The mirror: this terminal was asked for the SESSION's modes at the claim,
559 # not for the tile's own set — and 1003 is outside that set.
560 grep -qaF "$(printf '\033')[?1003h" "$OUT.zm2cap" || {
561 echo "e2e FAIL: app mouse: the claim never mirrored the session's any-motion mode:"
562 cat "$OUT.zm2pc"; exit 1; }
563 # And the negative that makes the pair a pair: this session has the same 77
564 # rows of history as the leg above, and the same wheel byte moved none of it.
565 grep -qa "mln60" "$OUT.zm2cap" && {
566 echo "e2e FAIL: app mouse: the tile scrolled back on a wheel the app owned:"
567 cat "$OUT.zm2pc"; exit 1; }
568 assert_stopped "$SOCK42" "$D39PID" "app mouse" "$OUT.zm2stop"
569 D39PID=""
570 ok "an application in the focused tile gets the wheel, and the tile does not"
571
572 # ---- a tile whose pump has died still says something -------------------
573 #
574 # A dead tile keeps its rect and narrates on its label bar — the only
575 # paint a pump with nobody left to answer `repaint_gen` ever does. The
576 # keyboard is the only thread left, and it draws that bar so the wall
577 # never silently loses a row.
578 #
579 # The tile here is dead by the most ordinary route: a spelling naming a
580 # session that does not exist. The daemon refuses the attach and that pump
581 # ends — `[refused]`, the state the wall already had a word for. Its bar
582 # says so from the first paint, and the live tile beside it is unharmed:
583 # the wall is still a wall, and the focus stays on the tile that can be
584 # typed at.
585 #
586 # Deliberately NOT asserted: that a `[reconnecting]` tile gets the same
587 # treatment. It must not — it still has a thread that will repaint its hot
588 # replica within a poll timeout, and the keyboard drawing over that would
589 # replace something true with something stale.
590 start_daemon "$SOCK39" "$OUT.zd.d" "dead-tile daemon never bound" --shell /bin/sh
591 D36PID=$DPID
592 pipe_mux "$OUT.zda" "$OUT.zda.err" timeout 40 "$MUX" --sock "$SOCK39" --session a
593 pipe_send 'printf "zdlive-%%s\\n" pin\n'
594 await_out "$OUT.zda" "zdlive-pin" "zdlive-pin never reached the client"
595 pipe_detach
596 wait_grid "$SOCK39" "zdlive-pin" "dead tile: the live session's marker" a
597
598 set +e
599 # tall: two tiles at 90x24 trip the aspect rule (90 >= 48); 70x36 stays
600 # stacked (70 < 72) and fits the bar label the assertion greps for
601 timeout 90 "$PTYCLIENT" --cols 70 --rows 36 --out "$OUT.zdcap" --err "$OUT.zdcap.err" -- \
602 "$MUX" wall "--sock $SOCK39#a" "--sock $SOCK39#ghost" > "$OUT.zdpc" 2>&1 <<'EOF'
603 expect zdlive-pin 20000
604 settle 800 20000
605 send printf 'zdagain-%s\\n' pin\n
606 expect zdagain-pin 15000
607 settle 400 15000
608 send \x1c2
609 settle 400 15000
610 send \x1c:abc
611 settle 400 15000
612 send \x1b
613 settle 400 15000
614 send \x1c:zz#bad name
615 settle 400 15000
616 send \r
617 expect [bad target 10000
618 settle 600 15000
619 send \x1c1
620 settle 400 15000
621 send \x1cd
622 waitexit 10000
623 EOF
624 RC=$?
625 set -e
626 [ "$RC" -eq 0 ] || {
627 echo "e2e FAIL: dead tile: ptyclient leg exited $RC:"
628 cat "$OUT.zdpc"; exit 1; }
629 # The dead tile narrated its own refusal: the state word the bar can only
630 # have if the pump reached the refusal path. Painted from the first
631 # Painted from the first relayout, and never cleared.
632 grep -q -- "--sock $SOCK39#ghost \[refused\]" "$OUT.zdcap" || {
633 echo "e2e FAIL: dead tile: the dead tile's bar never said [refused]"; exit 1; }
634 # The live tile is unharmed and answering — the wall is still a wall.
635 grep -q -- "--sock $SOCK39#a \[up\]" "$OUT.zdcap" || {
636 echo "e2e FAIL: dead tile: the live tile's bar never said [up]"; exit 1; }
637 # The focus stayed on the live tile: the marker typed after the wall came
638 # up landed in a, which is the tile that can be typed at. A focus that
639 # landed on the dead one would have dropped these bytes.
640 timeout 20 "$MUXA" capture --sock "$SOCK39" --session a > "$OUT.zdcapa" 2>&1
641 grep -q "zdagain-pin" "$OUT.zdcapa" || {
642 echo "e2e FAIL: dead tile: the live session never got the focused marker:"
643 cat "$OUT.zdcapa"; exit 1; }
644 # `Ctrl-\ :` is the one chord that works with the focus on a dead tile —
645 # the splits go through the pump's `ask`, and a dead pump answers nothing —
646 # so it is the one chord that can leave a line on a row nothing repaints.
647 # The oracle, not the byte stream: a banner LEFT on the screen is in the
648 # capture either way, and only the render says what the human was still
649 # looking at.
650 "$RENDER" --cols 70 --rows 36 < "$OUT.zdcap" > "$OUT.zdgrid" || {
651 echo "e2e FAIL: dead tile: render oracle failed"
652 cat "$OUT.zdgrid"; exit 1; }
653 # The refusal reached the eyes that earned it. Nothing claims a dead tile,
654 # so a notice only the claim path can show would never have been seen.
655 grep -q "\[bad target" "$OUT.zdgrid" || {
656 echo "e2e FAIL: dead tile: a refused prompt never narrated on the dead tile:"
657 cat "$OUT.zdgrid"; exit 1; }
658 grep -q ": zz#bad" "$OUT.zdgrid" && {
659 echo "e2e FAIL: dead tile: the refused prompt is still on the dead tile's row:"
660 cat "$OUT.zdgrid"; exit 1; }
661 # And Esc leaves nothing behind either: same row, same missing claim.
662 grep -q ": abc" "$OUT.zdgrid" && {
663 echo "e2e FAIL: dead tile: the cancelled prompt is still on the dead tile's row:"
664 cat "$OUT.zdgrid"; exit 1; }
665 assert_stopped "$SOCK39" "$D36PID" "dead tile" "$OUT.zdstop"
666 D36PID=""
667 ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one"