d9a5d6cb
test: e2e drives a real wheel from the pty side, both ways
a73x 2026-08-19 21:02
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -42,6 +42,16 @@ command -v curl > /dev/null 2>&1 || { | |||
| 42 | echo " hub's POST/DELETE/PUT answers); install it, or lose every" | 42 | echo " hub's POST/DELETE/PUT answers); install it, or lose every" |
| 43 | echo " check that the wall can be edited at all" | 43 | echo " check that the wall can be edited at all" |
| 44 | exit 1; } | 44 | exit 1; } |
| 45 | # And the third, for the same reason a third time: the alternate-screen | ||
| 46 | # wheel leg needs a REAL pager. A hand-rolled alt-screen script can prove | ||
| 47 | # arrows arrive, but only something that actually pages proves the user got | ||
| 48 | # what they reached for — and less is where the DECCKM trap lives (it reads | ||
| 49 | # `ESC O A`, not `ESC [ A`), which a stand-in would not reproduce. | ||
| 50 | command -v less > /dev/null 2>&1 || { | ||
| 51 | echo "e2e FAIL: this suite needs less (the alternate-screen wheel leg scrolls" | ||
| 52 | echo " a real pager); install it, or lose the only check that a" | ||
| 53 | echo " wheel notch moves anything on a TUI screen" | ||
| 54 | exit 1; } | ||
| 45 | 55 | ||
| 46 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" | 56 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" |
| 47 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" | 57 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" |
| @@ -286,6 +296,28 @@ D28PID="" | |||
| 286 | SOCK32="${TMPDIR:-/tmp}/muxd-e2e-wallzoom-$$.sock" | 296 | SOCK32="${TMPDIR:-/tmp}/muxd-e2e-wallzoom-$$.sock" |
| 287 | D29PID="" | 297 | D29PID="" |
| 288 | 298 | ||
| 299 | # The wheel pair. Each needs a scrollback of its own to scroll (or to prove | ||
| 300 | # it did not), which the long-lived /bin/sh daemon has no way to hold still, | ||
| 301 | # and the second one needs a session that ASKS for the mouse — a mode that | ||
| 302 | # would follow every other scenario sharing the daemon around. | ||
| 303 | SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock" | ||
| 304 | D30PID="" | ||
| 305 | WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh" | ||
| 306 | SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock" | ||
| 307 | D31PID="" | ||
| 308 | MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh" | ||
| 309 | # The alternate-screen half of the wheel: its own daemon because the | ||
| 310 | # session under test is a PAGER holding the alt screen for its whole life, | ||
| 311 | # which no other scenario could share a grid with. And the no-terminal | ||
| 312 | # half, whose session is a bare `cat` so that what reaches the pty is | ||
| 313 | # echoed back into the grid the assertion reads. | ||
| 314 | SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock" | ||
| 315 | D32PID="" | ||
| 316 | LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh" | ||
| 317 | LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt" | ||
| 318 | SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock" | ||
| 319 | D33PID="" | ||
| 320 | |||
| 289 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 321 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 290 | # such line on exit; every field is a key=value pair, so a rename or reorder | 322 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| 291 | # in the client shows up here as an empty read rather than a wrong number. | 323 | # in the client shows up here as an empty read rather than a wrong number. |
| @@ -812,6 +844,10 @@ cleanup() { | |||
| 812 | [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true | 844 | [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true |
| 813 | [ -n "$D28PID" ] && kill "$D28PID" 2>/dev/null || true | 845 | [ -n "$D28PID" ] && kill "$D28PID" 2>/dev/null || true |
| 814 | [ -n "$D29PID" ] && kill "$D29PID" 2>/dev/null || true | 846 | [ -n "$D29PID" ] && kill "$D29PID" 2>/dev/null || true |
| 847 | [ -n "$D30PID" ] && kill "$D30PID" 2>/dev/null || true | ||
| 848 | [ -n "$D31PID" ] && kill "$D31PID" 2>/dev/null || true | ||
| 849 | [ -n "$D32PID" ] && kill "$D32PID" 2>/dev/null || true | ||
| 850 | [ -n "$D33PID" ] && kill "$D33PID" 2>/dev/null || true | ||
| 815 | # The stops still precede the socket rm below, like SOCK14-17 above: | 851 | # The stops still precede the socket rm below, like SOCK14-17 above: |
| 816 | # unlinking a socket first would leave a live daemon nothing could reach | 852 | # unlinking a socket first would leave a live daemon nothing could reach |
| 817 | # by path. | 853 | # by path. |
| @@ -826,6 +862,10 @@ cleanup() { | |||
| 826 | [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true | 862 | [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true |
| 827 | [ -S "$SOCK31" ] && "$MUXD" stop --sock "$SOCK31" 2>/dev/null || true | 863 | [ -S "$SOCK31" ] && "$MUXD" stop --sock "$SOCK31" 2>/dev/null || true |
| 828 | [ -S "$SOCK32" ] && "$MUXD" stop --sock "$SOCK32" 2>/dev/null || true | 864 | [ -S "$SOCK32" ] && "$MUXD" stop --sock "$SOCK32" 2>/dev/null || true |
| 865 | [ -S "$SOCK33" ] && "$MUXD" stop --sock "$SOCK33" 2>/dev/null || true | ||
| 866 | [ -S "$SOCK34" ] && "$MUXD" stop --sock "$SOCK34" 2>/dev/null || true | ||
| 867 | [ -S "$SOCK35" ] && "$MUXD" stop --sock "$SOCK35" 2>/dev/null || true | ||
| 868 | [ -S "$SOCK36" ] && "$MUXD" stop --sock "$SOCK36" 2>/dev/null || true | ||
| 829 | 869 | ||
| 830 | # ---- the leak sweep (hygiene kit, 6a) ---- | 870 | # ---- the leak sweep (hygiene kit, 6a) ---- |
| 831 | # Here rather than at the bottom of the file, which `set -e` reaches only | 871 | # Here rather than at the bottom of the file, which `set -e` reaches only |
| @@ -838,7 +878,8 @@ cleanup() { | |||
| 838 | "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ | 878 | "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ |
| 839 | "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ | 879 | "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ |
| 840 | "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ | 880 | "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ |
| 841 | "$D26PID" "$D27PID" "$D28PID" "$D29PID" | 881 | "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ |
| 882 | "$D32PID" "$D33PID" | ||
| 842 | _leak=0 | 883 | _leak=0 |
| 843 | leak_sweep "$_rc" || _leak=1 | 884 | leak_sweep "$_rc" || _leak=1 |
| 844 | 885 | ||
| @@ -890,6 +931,15 @@ cleanup() { | |||
| 890 | # so without this line the residue survives exactly the runs where the | 931 | # so without this line the residue survives exactly the runs where the |
| 891 | # leg failed and somebody is looking at it. | 932 | # leg failed and somebody is looking at it. |
| 892 | rm -f "$OUT.title" "$OUT.title.err" "$OUT.title.log" "$OUT.title.sh" | 933 | rm -f "$OUT.title" "$OUT.title.err" "$OUT.title.log" "$OUT.title.sh" |
| 934 | # The wheel pair: two captures, two fixture logs, two session scripts | ||
| 935 | # and the daemon logs beside them. | ||
| 936 | rm -f "$OUT.whl" "$OUT.whl.err" "$OUT.whl.log" "$OUT.whl.d" "$WHEELSH" \ | ||
| 937 | "$OUT.mse" "$OUT.mse.err" "$OUT.mse.log" "$OUT.mse.d" "$MOUSESH" \ | ||
| 938 | "$SOCK33" "$SOCK34" "$OUT.whlcap" "$OUT.msecap" \ | ||
| 939 | "$OUT.whlstop" "$OUT.msestop" | ||
| 940 | rm -f "$OUT.pgr" "$OUT.pgr.err" "$OUT.pgr.log" "$OUT.pgr.d" "$LESSSH" \ | ||
| 941 | "$LESSDATA" "$OUT.pgrcap" "$OUT.pgrstop" "$SOCK35" \ | ||
| 942 | "$OUT.pipe" "$OUT.pipe.d" "$OUT.pipecap" "$OUT.pipestop" "$SOCK36" | ||
| 893 | # ...and the non-tty capture that leg's session feeds. | 943 | # ...and the non-tty capture that leg's session feeds. |
| 894 | rm -f "$OUT.nogate" | 944 | rm -f "$OUT.nogate" |
| 895 | # ...and its other half: the paste capture and the file nvim wrote, which | 945 | # ...and its other half: the paste capture and the file nvim wrote, which |
| @@ -2571,11 +2621,11 @@ done | |||
| 2571 | MUX_PREDICT_STATS=1 timeout 60 "$PTYCLIENT" --cols 80 --rows 24 \ | 2621 | MUX_PREDICT_STATS=1 timeout 60 "$PTYCLIENT" --cols 80 --rows 24 \ |
| 2572 | --out "$OUT.tp1" --err "$OUT.tp1.err" -- \ | 2622 | --out "$OUT.tp1" --err "$OUT.tp1.err" -- \ |
| 2573 | "$MUX" --via "$MUXD proxy --sock $SOCK13" > "$OUT.tp1.log" 2>&1 <<'EOF' & | 2623 | "$MUX" --via "$MUXD proxy --sock $SOCK13" > "$OUT.tp1.log" 2>&1 <<'EOF' & |
| 2574 | expect 100 10000 | 2624 | expect \x1b[0m100 10000 |
| 2575 | send \x1b[5;2~ | 2625 | send \x1b[5;2~ |
| 2576 | expect 60 10000 | 2626 | expect 60 10000 |
| 2577 | expect reconnecting 20000 | 2627 | expect reconnecting 20000 |
| 2578 | expect 100 20000 | 2628 | expect \x1b[0m100 20000 |
| 2579 | send t | 2629 | send t |
| 2580 | expect \x1b[2K\x1b[0mt 20000 | 2630 | expect \x1b[2K\x1b[0mt 20000 |
| 2581 | settle 500 15000 | 2631 | settle 500 15000 |
| @@ -2588,7 +2638,12 @@ TP1PID=$! | |||
| 2588 | # the fixture's "done 3" line is the barrier. Every needle above is unique | 2638 | # the fixture's "done 3" line is the barrier. Every needle above is unique |
| 2589 | # to its phase, which is what makes each expect sound under the no-counting | 2639 | # to its phase, which is what makes each expect sound under the no-counting |
| 2590 | # rule: | 2640 | # rule: |
| 2591 | # the first "100" is the attach snapshot's last row; | 2641 | # the first "100" is the attach snapshot's last row, and it is spelled |
| 2642 | # with the SGR reset the paint puts in front of every row because a bare | ||
| 2643 | # "100" is no longer unique to it: the client asks its own terminal for | ||
| 2644 | # mouse reporting with `\x1b[?1000h`, which lands in this capture before | ||
| 2645 | # any row does. Measured, not feared — that is the collision that made | ||
| 2646 | # this scenario fail when the wheel landed; | ||
| 2592 | # "60" is in the history page (54..77) and in NOTHING before it. The | 2647 | # "60" is in the history page (54..77) and in NOTHING before it. The |
| 2593 | # snapshot's rows are 78..100, and the escapes around them cannot spell | 2648 | # snapshot's rows are 78..100, and the escapes around them cannot spell |
| 2594 | # it either — but that is a fact about THIS session, not about the paint | 2649 | # it either — but that is a fact about THIS session, not about the paint |
| @@ -2598,7 +2653,7 @@ TP1PID=$! | |||
| 2598 | # mode with one keystroke typed after the tear, so that column is 1 or 2 | 2653 | # mode with one keystroke typed after the tear, so that column is 1 or 2 |
| 2599 | # for the whole scenario, and the row half of a CUP tops out at 24; | 2654 | # for the whole scenario, and the row half of a CUP tops out at 24; |
| 2600 | # "reconnecting" is the banner, painted once, on the tear; | 2655 | # "reconnecting" is the banner, painted once, on the tear; |
| 2601 | # the SECOND "100" can only be the post-resync repaint: the scroll view | 2656 | # the SECOND "\x1b[0m100" can only be the post-resync repaint: the scroll view |
| 2602 | # holds 54..77, and the banner carries no digits but its own position; | 2657 | # holds 54..77, and the banner carries no digits but its own position; |
| 2603 | # the last needle is STRUCTURAL, and it has to be. In canonical mode the | 2658 | # the last needle is STRUCTURAL, and it has to be. In canonical mode the |
| 2604 | # only thing a single keystroke produces is the line discipline's echo of | 2659 | # only thing a single keystroke produces is the line discipline's echo of |
| @@ -5013,6 +5068,257 @@ assert_stopped "$SOCK32" "$D29PID" "wall zoom" "$OUT.wzstop" | |||
| 5013 | D29PID="" | 5068 | D29PID="" |
| 5014 | ok "the wall zooms: Enter types into the selected session, Ctrl-\\ d comes back" | 5069 | ok "the wall zooms: Enter types into the selected session, Ctrl-\\ d comes back" |
| 5015 | 5070 | ||
| 5071 | # ---- the wheel scrolls back, and an app that asks gets it instead ------- | ||
| 5072 | # | ||
| 5073 | # The pair is one claim seen from both sides, so the two legs are the same | ||
| 5074 | # session, the same bytes and opposite outcomes: a wheel report belongs to | ||
| 5075 | # the scrollback until an application inside the session asks for the mouse, | ||
| 5076 | # and then it belongs to that application. | ||
| 5077 | # | ||
| 5078 | # The bytes are real ones: `\x1b[<64;10;5M` is what a terminal writes for a | ||
| 5079 | # wheel-up press once the client has asked for SGR reports (1006), which is | ||
| 5080 | # the point — before this the client asked for nothing, so a terminal | ||
| 5081 | # answered the wheel by SYNTHESISING arrow keys on the alternate screen | ||
| 5082 | # (DEC 1007) and the session read them as input. | ||
| 5083 | # | ||
| 5084 | # (a) At the live view, eight notches in ONE write scroll a screenful back. | ||
| 5085 | # Eight because a notch is three rows: 8*3 = 24, exactly what | ||
| 5086 | # Shift+PageUp moves on this 80x24 terminal, which is what lets this | ||
| 5087 | # leg borrow tp1's needle — "60" is in the history page (rows 54..77) | ||
| 5088 | # and in nothing painted before it. One write rather than eight because | ||
| 5089 | # that is how a terminal writes a spin, and because a burst that is not | ||
| 5090 | # SUMMED lands three rows up instead of twenty-four: the needle fails. | ||
| 5091 | cat > "$WHEELSH" <<'EOF' | ||
| 5092 | #!/bin/sh | ||
| 5093 | seq 1 100 | ||
| 5094 | exec /bin/cat | ||
| 5095 | EOF | ||
| 5096 | chmod +x "$WHEELSH" | ||
| 5097 | "$MUXD" run --sock "$SOCK33" --shell "$WHEELSH" > "$OUT.whl.d" 2>&1 & | ||
| 5098 | D30PID=$! | ||
| 5099 | wait_sock "$SOCK33" "$OUT.whl.d" "wheel daemon never bound" | ||
| 5100 | # Attach only once seq has finished, for tp1's reason: the page the wheel | ||
| 5101 | # fetches has to be content this client was never sent. | ||
| 5102 | i=0 | ||
| 5103 | until "$MUXD" dump --sock "$SOCK33" | grep -q "100"; do | ||
| 5104 | i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: wheel session never finished seq"; exit 1; } | ||
| 5105 | sleep 0.1 | ||
| 5106 | done | ||
| 5107 | # `\x1b[0m100`, not "100": the client asks its own terminal for mouse | ||
| 5108 | # reporting with `\x1b[?1000h`, so a bare "100" is in this capture before | ||
| 5109 | # any row is. The SGR reset in front of it is the paint's, and nothing else | ||
| 5110 | # writes that pair. (Measured: with the plain needle this leg passes on the | ||
| 5111 | # mode enable and sends the wheel before the snapshot has landed.) | ||
| 5112 | set +e | ||
| 5113 | timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.whl" --err "$OUT.whl.err" \ | ||
| 5114 | -- "$MUX" --sock "$SOCK33" > "$OUT.whl.log" 2>&1 <<'EOF' | ||
| 5115 | expect \x1b[0m100 15000 | ||
| 5116 | 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 | ||
| 5117 | expect 60 15000 | ||
| 5118 | 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 | ||
| 5119 | expect \x1b[0m100 15000 | ||
| 5120 | send \x1b[<64;10;5Mwheel-and-key | ||
| 5121 | settle 600 15000 | ||
| 5122 | send \x1cd | ||
| 5123 | waitexit 10000 | ||
| 5124 | EOF | ||
| 5125 | RC=$? | ||
| 5126 | set -e | ||
| 5127 | [ "$RC" -eq 0 ] || { | ||
| 5128 | echo "e2e FAIL: wheel: ptyclient leg exited $RC (did the wheel scroll, and come back?):" | ||
| 5129 | cat "$OUT.whl.log"; exit 1; } | ||
| 5130 | # The other half of "the wheel is the client's": the bytes are CONSUMED. | ||
| 5131 | # Seventeen mouse reports were typed at a shell that would have echoed every | ||
| 5132 | # one of them, and the session's grid must hold none. | ||
| 5133 | timeout 20 "$MUXA" capture --sock "$SOCK33" > "$OUT.whlcap" 2>&1 | ||
| 5134 | grep -qF -- "[<64;" "$OUT.whlcap" && { | ||
| 5135 | echo "e2e FAIL: wheel: a mouse report reached the pty as input:" | ||
| 5136 | cat "$OUT.whlcap"; exit 1; } | ||
| 5137 | # ...but a keystroke that shared its read with a notch is NOT consumed. The | ||
| 5138 | # last send was one write holding both, typed at the live view: the wheel | ||
| 5139 | # moved the view and the letters belong to the shell. Swallowing them as | ||
| 5140 | # "the key that leaves scroll mode" loses input to a view the user had not | ||
| 5141 | # seen yet, which is what this needle catches. | ||
| 5142 | grep -qF -- "wheel-and-key" "$OUT.whlcap" || { | ||
| 5143 | echo "e2e FAIL: wheel: a keystroke sharing a read with a notch never reached the pty:" | ||
| 5144 | cat "$OUT.whlcap"; exit 1; } | ||
| 5145 | assert_stopped "$SOCK33" "$D30PID" "wheel" "$OUT.whlstop" | ||
| 5146 | D30PID="" | ||
| 5147 | ok "the wheel scrolls back and returns to live, and never reaches the pty" | ||
| 5148 | |||
| 5149 | # (b) The same session with an application holding the mouse. The script | ||
| 5150 | # asks for tracking (1000), drag (1002) and SGR reports (1006) — vim's | ||
| 5151 | # `set mouse=a` set — before becoming cat, so every wheel byte is the | ||
| 5152 | # application's and the client must hand them over untouched. | ||
| 5153 | # | ||
| 5154 | # 1002 is the mode that makes this leg sharp: the client's OWN capture | ||
| 5155 | # set is 1000+1006, so a `?1002h` on this terminal can only have come | ||
| 5156 | # from mirroring what the session asked for. | ||
| 5157 | cat > "$MOUSESH" <<'EOF' | ||
| 5158 | #!/bin/sh | ||
| 5159 | seq 1 100 | ||
| 5160 | printf '\033[?1000h\033[?1002h\033[?1006h' | ||
| 5161 | printf 'app-holds-the-mouse\n' | ||
| 5162 | exec /bin/cat | ||
| 5163 | EOF | ||
| 5164 | chmod +x "$MOUSESH" | ||
| 5165 | "$MUXD" run --sock "$SOCK34" --shell "$MOUSESH" > "$OUT.mse.d" 2>&1 & | ||
| 5166 | D31PID=$! | ||
| 5167 | wait_sock "$SOCK34" "$OUT.mse.d" "app-mouse daemon never bound" | ||
| 5168 | i=0 | ||
| 5169 | until "$MUXD" dump --sock "$SOCK34" | grep -q "app-holds-the-mouse"; do | ||
| 5170 | i=$((i+1)); [ "$i" -lt 100 ] || { echo "e2e FAIL: app-mouse session never armed"; exit 1; } | ||
| 5171 | sleep 0.1 | ||
| 5172 | done | ||
| 5173 | # The modes are already set when this client attaches, so they arrive in | ||
| 5174 | # the attach's own term_modes and there is no race to settle for. The echo | ||
| 5175 | # is the assertion: cat is in canonical mode with ECHOCTL, so bytes that | ||
| 5176 | # reach the pty come back as `^[[<64;10;5M` and bytes that do not, do not. | ||
| 5177 | set +e | ||
| 5178 | timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.mse" --err "$OUT.mse.err" \ | ||
| 5179 | -- "$MUX" --sock "$SOCK34" > "$OUT.mse.log" 2>&1 <<'EOF' | ||
| 5180 | expect app-holds-the-mouse 15000 | ||
| 5181 | send \x1b[<64;10;5M | ||
| 5182 | expect [<64;10;5M 15000 | ||
| 5183 | send \x1cd | ||
| 5184 | waitexit 10000 | ||
| 5185 | EOF | ||
| 5186 | RC=$? | ||
| 5187 | set -e | ||
| 5188 | [ "$RC" -eq 0 ] || { | ||
| 5189 | echo "e2e FAIL: app mouse: ptyclient leg exited $RC (did the wheel reach the app?):" | ||
| 5190 | cat "$OUT.mse.log"; exit 1; } | ||
| 5191 | # The session's own grid, not just the client's screen: the echo is the pty | ||
| 5192 | # saying it received the bytes. | ||
| 5193 | timeout 20 "$MUXA" capture --sock "$SOCK34" > "$OUT.msecap" 2>&1 | ||
| 5194 | grep -qF -- "[<64;10;5M" "$OUT.msecap" || { | ||
| 5195 | echo "e2e FAIL: app mouse: the wheel never reached the application's pty:" | ||
| 5196 | cat "$OUT.msecap"; exit 1; } | ||
| 5197 | # The mirror: this terminal was asked for the session's modes, not the | ||
| 5198 | # client's own set. | ||
| 5199 | grep -qaF "$(printf '\033[?1002h')" "$OUT.mse" || { | ||
| 5200 | echo "e2e FAIL: app mouse: the client never mirrored the session's drag mode"; exit 1; } | ||
| 5201 | # And the negative that makes the pair a pair: this session has the same | ||
| 5202 | # 77 rows of history as (a), and the same wheel byte moved none of it. | ||
| 5203 | grep -qF -- "60" "$OUT.mse" && { | ||
| 5204 | echo "e2e FAIL: app mouse: the client scrolled back on a wheel the app owned"; exit 1; } | ||
| 5205 | assert_stopped "$SOCK34" "$D31PID" "app mouse" "$OUT.msestop" | ||
| 5206 | D31PID="" | ||
| 5207 | ok "an application that asked for the mouse gets the wheel, and the client does not" | ||
| 5208 | |||
| 5209 | # ---- the wheel on the alternate screen: a pager, not our scrollback ---- | ||
| 5210 | # | ||
| 5211 | # The alt screen has no scrollback of ours — `historyRows` is 0 there by | ||
| 5212 | # contract — so a notch that fell through to the scroll arithmetic was | ||
| 5213 | # consumed by the mouse filter and then dropped. Measured on this very | ||
| 5214 | # scenario before the fix: four notches, grid unmoved. | ||
| 5215 | # | ||
| 5216 | # `less +G` on 200 lines: the view starts at the END, so a wheel-UP is the | ||
| 5217 | # direction with somewhere to go. Eight notches is 24 rows, one screenful, | ||
| 5218 | # which moves the top from 178 to 154 — and 178 leaves the screen entirely, | ||
| 5219 | # so the assertion has both a needle and its negative. | ||
| 5220 | # | ||
| 5221 | # LESS is cleared in the script rather than trusted: an operator with | ||
| 5222 | # `LESS=--mouse` exported would have a pager that DOES ask for the mouse, | ||
| 5223 | # which is the other leg's story, and this one would fail for a reason that | ||
| 5224 | # has nothing to do with the code. | ||
| 5225 | seq 1 200 > "$LESSDATA" | ||
| 5226 | cat > "$LESSSH" <<EOF | ||
| 5227 | #!/bin/sh | ||
| 5228 | LESS= | ||
| 5229 | export LESS | ||
| 5230 | exec less +G $LESSDATA | ||
| 5231 | EOF | ||
| 5232 | chmod +x "$LESSSH" | ||
| 5233 | "$MUXD" run --sock "$SOCK35" --shell "$LESSSH" > "$OUT.pgr.d" 2>&1 & | ||
| 5234 | D32PID=$! | ||
| 5235 | wait_sock "$SOCK35" "$OUT.pgr.d" "pager daemon never bound" | ||
| 5236 | i=0 | ||
| 5237 | until "$MUXD" dump --sock "$SOCK35" | grep -qx "200"; do | ||
| 5238 | i=$((i+1)); [ "$i" -lt 100 ] || { | ||
| 5239 | echo "e2e FAIL: pager never reached the end of the file:" | ||
| 5240 | "$MUXD" dump --sock "$SOCK35" | tail -3; exit 1; } | ||
| 5241 | sleep 0.1 | ||
| 5242 | done | ||
| 5243 | set +e | ||
| 5244 | timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \ | ||
| 5245 | -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF' | ||
| 5246 | expect 199 15000 | ||
| 5247 | 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 | ||
| 5248 | settle 800 15000 | ||
| 5249 | send \x1cd | ||
| 5250 | waitexit 10000 | ||
| 5251 | EOF | ||
| 5252 | RC=$? | ||
| 5253 | set -e | ||
| 5254 | [ "$RC" -eq 0 ] || { | ||
| 5255 | echo "e2e FAIL: pager: ptyclient leg exited $RC:"; cat "$OUT.pgr.log"; exit 1; } | ||
| 5256 | # `muxd dump`, not `muxa capture`: the assertions below are WHOLE-LINE ones | ||
| 5257 | # and capture answers in JSON, where the grid is one line with escaped | ||
| 5258 | # newlines in it — a `grep -x` against that can never match. (It did not, | ||
| 5259 | # on the first run of this leg, while the grid underneath was exactly | ||
| 5260 | # right.) dump prints the grid a row per line, which is what -x needs. | ||
| 5261 | "$MUXD" dump --sock "$SOCK35" > "$OUT.pgrcap" 2>&1 | ||
| 5262 | # A whole line, because the pager's status line carries the file's PATH — | ||
| 5263 | # which holds this run's pid and could spell any short number. | ||
| 5264 | grep -qx "154" "$OUT.pgrcap" || { | ||
| 5265 | echo "e2e FAIL: pager: the wheel moved nothing on the alternate screen:" | ||
| 5266 | cat "$OUT.pgrcap"; exit 1; } | ||
| 5267 | grep -qx "178" "$OUT.pgrcap" && { | ||
| 5268 | echo "e2e FAIL: pager: the old top row is still on screen, so the view did not move a screenful:" | ||
| 5269 | cat "$OUT.pgrcap"; exit 1; } | ||
| 5270 | # The pager never asked for the mouse, so it must never have SEEN a mouse | ||
| 5271 | # report: what reached it was arrow keys, which leave no text behind. | ||
| 5272 | grep -qF -- "[<64;" "$OUT.pgrcap" && { | ||
| 5273 | echo "e2e FAIL: pager: a raw mouse report reached an application that never asked for one:" | ||
| 5274 | cat "$OUT.pgrcap"; exit 1; } | ||
| 5275 | assert_stopped "$SOCK35" "$D32PID" "pager" "$OUT.pgrstop" | ||
| 5276 | D32PID="" | ||
| 5277 | ok "the wheel scrolls a pager on the alternate screen, as arrow keys" | ||
| 5278 | |||
| 5279 | # ---- a client with no terminal filters nothing -------------------------- | ||
| 5280 | # | ||
| 5281 | # The mouse filter is gated on this client having TAKEN a terminal over, | ||
| 5282 | # and that gate has to be the alt-screen flag rather than the mode bits: | ||
| 5283 | # a client whose stdin is a pipe never asked anyone for mouse reports, so | ||
| 5284 | # nothing it reads can be one. Filtering there is pure loss — the bytes are | ||
| 5285 | # whatever a script piped in, and an SGR-shaped escape in a heredoc is text | ||
| 5286 | # somebody meant to send. | ||
| 5287 | # | ||
| 5288 | # `cat` as the session, so what arrives is echoed back and the grid IS the | ||
| 5289 | # assertion: the bytes reached the pty or they did not. | ||
| 5290 | "$MUXD" run --sock "$SOCK36" --shell /bin/cat > "$OUT.pipe.d" 2>&1 & | ||
| 5291 | D33PID=$! | ||
| 5292 | wait_sock "$SOCK36" "$OUT.pipe.d" "pipe-stdin daemon never bound" | ||
| 5293 | # The detach chord is a SEPARATE write behind a sleep, and both halves of | ||
| 5294 | # that are load-bearing. Without it the client stays attached after stdin | ||
| 5295 | # ends — a pipe closing is not a session ending — and the leg would prove | ||
| 5296 | # its point by timing out. And it cannot ride the same write as the | ||
| 5297 | # payload: a chord ENDS its chunk (PrefixFilter), so bytes ahead of it in | ||
| 5298 | # the same read are dropped, which would race the assertion away. | ||
| 5299 | { printf 'hello \033[<64;10;5M world\n'; sleep 1; printf '\034d'; } | | ||
| 5300 | timeout 20 "$MUX" --sock "$SOCK36" > "$OUT.pipe" 2>&1 | ||
| 5301 | RC=$? | ||
| 5302 | [ "$RC" -eq 0 ] || { | ||
| 5303 | echo "e2e FAIL: pipe stdin: the client exited $RC:"; cat "$OUT.pipe"; exit 1; } | ||
| 5304 | i=0 | ||
| 5305 | until timeout 20 "$MUXA" capture --sock "$SOCK36" 2>&1 | grep -qF -- "world"; do | ||
| 5306 | i=$((i+1)); [ "$i" -lt 100 ] || { | ||
| 5307 | echo "e2e FAIL: pipe stdin: the piped line never reached the pty:" | ||
| 5308 | timeout 20 "$MUXA" capture --sock "$SOCK36"; exit 1; } | ||
| 5309 | sleep 0.1 | ||
| 5310 | done | ||
| 5311 | timeout 20 "$MUXA" capture --sock "$SOCK36" > "$OUT.pipecap" 2>&1 | ||
| 5312 | # The whole line, escape included. `cat` echoes control bytes as `^[`, so | ||
| 5313 | # what a grid can show of `\x1b[<64;10;5M` is `[<64;10;5M` — and a client | ||
| 5314 | # that ate the report leaves `hello world` with the middle missing. | ||
| 5315 | grep -qF -- "[<64;10;5M" "$OUT.pipecap" || { | ||
| 5316 | echo "e2e FAIL: pipe stdin: an SGR-shaped escape was filtered out of a stream nobody was reporting on:" | ||
| 5317 | cat "$OUT.pipecap"; exit 1; } | ||
| 5318 | assert_stopped "$SOCK36" "$D33PID" "pipe stdin" "$OUT.pipestop" | ||
| 5319 | D33PID="" | ||
| 5320 | ok "a client with no terminal of its own forwards SGR-shaped bytes untouched" | ||
| 5321 | |||
| 5016 | # The long-lived daemon has served every scenario that wanted it; stop it | 5322 | # The long-lived daemon has served every scenario that wanted it; stop it |
| 5017 | # NOW so its allocator verdict is written while the suite is still running | 5323 | # NOW so its allocator verdict is written while the suite is still running |
| 5018 | # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain | 5324 | # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain |
| @@ -5028,7 +5334,7 @@ DPID="" | |||
| 5028 | 5334 | ||
| 5029 | # The pins. Literals, not variables set from counting something else — | 5335 | # The pins. Literals, not variables set from counting something else — |
| 5030 | # "assert the literal, never the constant the code under test reads" | 5336 | # "assert the literal, never the constant the code under test reads" |
| 5031 | # (decisions.md, M10). 39 scenario checkpoints; 35 convergence points. | 5337 | # (decisions.md, M10). 43 scenario checkpoints; 35 convergence points. |
| 5032 | # Anyone adding a scenario updates these by hand, on purpose. | 5338 | # Anyone adding a scenario updates these by hand, on purpose. |
| 5033 | # | 5339 | # |
| 5034 | # M18 added three checkpoints and no convergence points: its wall block | 5340 | # M18 added three checkpoints and no convergence points: its wall block |
| @@ -5070,9 +5376,23 @@ DPID="" | |||
| 5070 | # reason a third time: its subject is a ptyclient capture — one terminal | 5376 | # reason a third time: its subject is a ptyclient capture — one terminal |
| 5071 | # torn down and rebuilt around a child client — and the two grid checks it | 5377 | # torn down and rebuilt around a child client — and the two grid checks it |
| 5072 | # does make are `muxa status` sizes, which assert_converged does not speak | 5378 | # does make are `muxa status` sizes, which assert_converged does not speak |
| 5073 | # about at all. | 5379 | # about at all. The 40th is the wheel, and no convergence point because |
| 5074 | [ "$OK_COUNT" = "39" ] || { | 5380 | # what it asserts on is a SCROLLBACK view — rows the live grid does not |
| 5075 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 39 —" | 5381 | # hold, which is the whole reason to scroll — plus the absence of bytes |
| 5382 | # from a grid, which converging two identical empties would say nothing | ||
| 5383 | # about. The 41st is its twin, the application that owns the mouse, and no | ||
| 5384 | # convergence point for the side-channel reason: its subject is the modes | ||
| 5385 | # the client wrote to a terminal and the echo of bytes it forwarded, and a | ||
| 5386 | # grid carries the second without carrying the first. The 42nd is the wheel | ||
| 5387 | # on the ALTERNATE screen, and no convergence point because the grid it | ||
| 5388 | # asserts on belongs to a pager holding that screen — assert_converged | ||
| 5389 | # replays a capture into an engine and compares, and a capture whose whole | ||
| 5390 | # subject is a full-screen application's redraws is one this suite has | ||
| 5391 | # never claimed to reproduce byte for byte. The 43rd is the client with no | ||
| 5392 | # terminal of its own, and no convergence point because it HAS no terminal: | ||
| 5393 | # there is no capture to converge, which is the entire point of the leg. | ||
| 5394 | [ "$OK_COUNT" = "43" ] || { | ||
| 5395 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 43 —" | ||
| 5076 | echo " a scenario was added (update the pin) or silently lost" | 5396 | echo " a scenario was added (update the pin) or silently lost" |
| 5077 | exit 1 | 5397 | exit 1 |
| 5078 | } | 5398 | } |
| @@ -5080,4 +5400,4 @@ DPID="" | |||
| 5080 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" | 5400 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" |
| 5081 | exit 1 | 5401 | exit 1 |
| 5082 | } | 5402 | } |
| 5083 | echo "e2e OK (39 scenarios, 35 convergence points)" | 5403 | echo "e2e OK (43 scenarios, 35 convergence points)" |