5b97e538
test: 08_mouse moves to a file of its own
a73x 2026-08-26 19:06
Commit message
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" | ||