fc698819
test: 07_wallcli moves to a file of its own
a73x 2026-08-26 19:00
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' | 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli' |
| 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,457 +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 CLI wall (`mux wall`): its own daemon, so its two sessions can't be | ||
| 149 | # confused with any other block's. | ||
| 150 | SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock" | ||
| 151 | defer_sock "$SOCK26" | ||
| 152 | # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord | ||
| 153 | # that fails to detach cannot strand another block's session. | ||
| 154 | SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" | ||
| 155 | defer_sock "$SOCK27" | ||
| 156 | # Ctrl-\ c (create a session and switch to it): its own daemon, because the | ||
| 157 | # assertion is about which sessions EXIST — another block's session on the | ||
| 158 | # same daemon would change the name the chord picks. | ||
| 159 | SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock" | ||
| 160 | defer_sock "$SOCK28" | ||
| 161 | # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same | ||
| 162 | # reason as SOCK28, and more sharply — the ring IS the list of sessions on | ||
| 163 | # one daemon, so a stray session from another block would change where a | ||
| 164 | # step lands. | ||
| 165 | SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" | ||
| 166 | defer_sock "$SOCK29" | ||
| 167 | # M5 (the self-attach refusal): its own daemon because the assertion reads | ||
| 168 | # the SOCKET PATH out of a session shell's environment and compares it to | ||
| 169 | # the one this block started — a shared daemon would make the comparison a | ||
| 170 | # tautology about whichever block bound first. | ||
| 171 | SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock" | ||
| 172 | defer_sock "$SOCK31" | ||
| 173 | |||
| 174 | # ---- the CLI wall: the hub's multiattach, in one terminal -------------- | ||
| 175 | # One daemon, two named sessions, one `mux wall` on a real pty showing | ||
| 176 | # both AT ONCE — the browser wall's claim, made by the CLI. Three facts, | ||
| 177 | # each with its own witness: both sessions' markers painted (the snapshot | ||
| 178 | # path), a line injected into b WHILE the wall is attached painted too | ||
| 179 | # (the delta path — the wall is a live replica, not a capture), and | ||
| 180 | # `\x1cd` ended the process with exit 0 (the wall left, not died). Markers | ||
| 181 | # are shell-EXPANDED (`%s`+pin), the M18 trick: a hit is the shell's work, | ||
| 182 | # never an echo of anything typed here. | ||
| 183 | # | ||
| 184 | # The one expect anchors on the injected delta, deliberately: tile pumps | ||
| 185 | # paint their first stripes in whichever order their snapshots land — a | ||
| 186 | # thread race, and ptyclient's expect cursor cannot bet on it. The | ||
| 187 | # injected delta is the one byte sequence guaranteed late (t+5s into a | ||
| 188 | # replica that must already hold b's snapshot); the snapshot markers are | ||
| 189 | # asserted order-free by the greps on the capture below. | ||
| 190 | # | ||
| 191 | # The two tiles are spelled DIFFERENTLY on purpose: `--sock PATH` as two | ||
| 192 | # arguments (muxweb's dialect) and as one quoted spelling (the wall file's). | ||
| 193 | # Both must reach the same tile, so both are pinned by this one leg. | ||
| 194 | start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh | ||
| 195 | D23PID=$DPID | ||
| 196 | |||
| 197 | pipe_mux "$OUT.cwa" "$OUT.cwa.err" timeout 40 "$MUX" --sock "$SOCK26" --session a | ||
| 198 | pipe_send 'printf "cwa-%%s\\n" pin\n' | ||
| 199 | await_out "$OUT.cwa" "cwa-pin" "cwa-pin never reached the client" | ||
| 200 | pipe_detach | ||
| 201 | wait_grid "$SOCK26" "cwa-pin" "CLI wall: session a's marker" a | ||
| 202 | pipe_mux "$OUT.cwb" "$OUT.cwb.err" timeout 40 "$MUX" --sock "$SOCK26" --session b | ||
| 203 | pipe_send 'printf "cwb-%%s\\n" pin\n' | ||
| 204 | await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client" | ||
| 205 | pipe_detach | ||
| 206 | wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b | ||
| 207 | |||
| 208 | ( sleep 5; "$MUXA" send 'printf "cwlive-%s\n" pin\n' \ | ||
| 209 | --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) & | ||
| 210 | CWINJPID=$! | ||
| 211 | defer_kill "$CWINJPID" | ||
| 212 | set +e | ||
| 213 | timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \ | ||
| 214 | "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF' | ||
| 215 | expect cwlive-pin 20000 | ||
| 216 | send \x1cd | ||
| 217 | waitexit 10000 | ||
| 218 | EOF | ||
| 219 | RC=$? | ||
| 220 | set -e | ||
| 221 | wait "$CWINJPID" 2>/dev/null || true | ||
| 222 | [ "$RC" -eq 0 ] || { | ||
| 223 | echo "e2e FAIL: CLI wall: ptyclient leg exited $RC:" | ||
| 224 | cat "$OUT.cwpc"; cat "$OUT.cwinj"; exit 1; } | ||
| 225 | # The multiattach fact itself: both sessions' bytes in ONE process's | ||
| 226 | # capture. Redundant with the expects above only until someone edits the | ||
| 227 | # script block; a grep each keeps the claim standing on its own. | ||
| 228 | grep -q "cwa-pin" "$OUT.cwcap" || { | ||
| 229 | echo "e2e FAIL: CLI wall: a's marker never painted"; exit 1; } | ||
| 230 | grep -q "cwb-pin" "$OUT.cwcap" || { | ||
| 231 | echo "e2e FAIL: CLI wall: b's marker never painted"; exit 1; } | ||
| 232 | grep -q "cwlive-pin" "$OUT.cwcap" || { | ||
| 233 | echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; } | ||
| 234 | # Every tile claims its rect: a and b were created 80-wide by pipe, and | ||
| 235 | # this 40-wide wall resized both on attach. There is no 0x0 passivity to | ||
| 236 | # assert — the wall writes the sessions it shows. | ||
| 237 | assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop" | ||
| 238 | D23PID="" | ||
| 239 | ok "mux wall: two sessions and a live delta on one terminal" | ||
| 240 | |||
| 241 | # ---- M1: Ctrl-\ is a prefix, not an action ------------------------------ | ||
| 242 | # | ||
| 243 | # Two claims in one client, because they are the same claim seen from both | ||
| 244 | # sides: the chord layer eats exactly the two bytes of a chord and nothing | ||
| 245 | # else. | ||
| 246 | # | ||
| 247 | # (a) an unknown command key is swallowed WITH its prefix. `\x1cz` is sent | ||
| 248 | # at an empty prompt (`z` is bound to nothing; `x` would forget the | ||
| 249 | # tile), then a marker command. If either byte had reached the pty the | ||
| 250 | # shell would have read `zprintf ...` and the marker would never | ||
| 251 | # print — so the marker is the assertion, and the explicit grep for | ||
| 252 | # `zprintf` names what went wrong when it does. | ||
| 253 | # (b) `\x1cd` detaches. The client exits 0 and the session it left behind | ||
| 254 | # still answers, which is what separates a detach from a kill. | ||
| 255 | # | ||
| 256 | # A real pty (ptyclient) rather than a pipeline: the shell only echoes what | ||
| 257 | # it was really given when there is a terminal to echo to, and the echo is | ||
| 258 | # what carries the stray `z` if the layer leaks one. | ||
| 259 | start_daemon "$SOCK27" "$OUT.pfx.d" "prefix daemon never bound" --shell /bin/sh | ||
| 260 | D24PID=$DPID | ||
| 261 | |||
| 262 | set +e | ||
| 263 | timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pfx" --err "$OUT.pfx.err" \ | ||
| 264 | -- "$MUX" --sock "$SOCK27" > "$OUT.pfx.log" 2>&1 <<'EOF' | ||
| 265 | expect \x1b[?1049h 15000 | ||
| 266 | settle 400 15000 | ||
| 267 | send \x1cz | ||
| 268 | settle 400 15000 | ||
| 269 | send printf 'm1-%s\\n' pin\n | ||
| 270 | expect m1-pin 15000 | ||
| 271 | settle 400 15000 | ||
| 272 | send \x1cd | ||
| 273 | waitexit 10000 | ||
| 274 | EOF | ||
| 275 | RC=$? | ||
| 276 | set -e | ||
| 277 | [ "$RC" -eq 0 ] || { | ||
| 278 | echo "e2e FAIL: prefix: ptyclient leg exited $RC (did \\x1cd detach?):" | ||
| 279 | cat "$OUT.pfx.log"; exit 1; } | ||
| 280 | grep -q "m1-pin" "$OUT.pfx" || { | ||
| 281 | echo "e2e FAIL: prefix: the marker never printed"; exit 1; } | ||
| 282 | grep -q "zprintf" "$OUT.pfx" && { | ||
| 283 | echo "e2e FAIL: prefix: the unknown command key reached the pty" | ||
| 284 | cat "$OUT.pfx"; exit 1; } | ||
| 285 | # Detached, not exited: the session outlives the client that left it. | ||
| 286 | "$MUXA" status --sock "$SOCK27" > "$OUT.pfxst" 2>&1 || { | ||
| 287 | echo "e2e FAIL: prefix: the session did not survive the detach:" | ||
| 288 | cat "$OUT.pfxst"; exit 1; } | ||
| 289 | assert_stopped "$SOCK27" "$D24PID" "prefix" "$OUT.pfxstop" | ||
| 290 | D24PID="" | ||
| 291 | ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches" | ||
| 292 | |||
| 293 | # ---- M2: Ctrl-\ c creates a session and switches to it ------------------- | ||
| 294 | # | ||
| 295 | # The claim is a MOVE, which takes two witnesses: the client ends up | ||
| 296 | # somewhere new, and the place it left is still there and unchanged. | ||
| 297 | # | ||
| 298 | # * one pty client attaches to the default session, runs a marker, types | ||
| 299 | # `\x1cc`, and runs a second marker. Both markers reach a shell — the | ||
| 300 | # second one proves the client is attached to a session that works, not | ||
| 301 | # merely alive after the chord. | ||
| 302 | # * `muxa status --session 1` proves the daemon really created the name | ||
| 303 | # the client picked (lowest free integer beside the default "0"). | ||
| 304 | # * the default session's capture still holds the FIRST marker and not the | ||
| 305 | # second. Without that pair the same log would be produced by a chord | ||
| 306 | # that did nothing at all. | ||
| 307 | # | ||
| 308 | # The leg then keeps pressing until the daemon runs out of sessions, because | ||
| 309 | # the refusal is the branch with the interesting answer: a refused attach is | ||
| 310 | # the one failure the zoom recovers from instead of exiting, and nothing | ||
| 311 | # else in the suite reaches it. See the block after the leg for the three | ||
| 312 | # things "recovered" has to mean. | ||
| 313 | # | ||
| 314 | # PHASE 3C evolved the WAIT, and only the wait. `\x1cc` used to tear the | ||
| 315 | # terminal down and build it back up — a fresh `?1049h` per switch — because | ||
| 316 | # each switch was a new client process on a new transport. Now `mux` IS the | ||
| 317 | # wall: the chord adds a tile and moves the ZOOM to it, and the alternate | ||
| 318 | # screen was entered once, at startup, and is never left. So the arrival | ||
| 319 | # signal is the zoom's own: `setFocus` clears the screen (`\x1b[2J`) on the | ||
| 320 | # thread that moves it, before the terminal changes hands. | ||
| 321 | # | ||
| 322 | # That needle is not merely "some paint": between the chord and the answer | ||
| 323 | # the old tile is idle and paints nothing, so the next `[2J` after the chord | ||
| 324 | # is the handover itself. Waiting for it MATTERS — keystrokes sent before it | ||
| 325 | # would go to the mailbox of the tile the user is leaving, which is the | ||
| 326 | # session they were typing in a moment ago and would silently swallow the | ||
| 327 | # marker. | ||
| 328 | start_daemon "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" --shell /bin/sh | ||
| 329 | D25PID=$DPID | ||
| 330 | # The refusal this leg's fourth chord asserts needs a FULL table. The | ||
| 331 | # chords make four (0, 1, 2, 3); the other 28 are filled here. `fillN` is | ||
| 332 | # not an integer, so nextFreeName still hands the chords 1, 2, 3 and | ||
| 333 | # reaches for "4" on the refused one — which is the name the post-check | ||
| 334 | # below asks about. | ||
| 335 | fill_sessions "$SOCK28" "$OUT.nswfill" fill 4 31 | ||
| 336 | |||
| 337 | set +e | ||
| 338 | # tall: Ctrl-\ c adds a second tile at 80x24 (80 >= 48) | ||
| 339 | timeout 40 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.nsw" --err "$OUT.nsw.err" \ | ||
| 340 | -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF' | ||
| 341 | expect \x1b[?1049h 15000 | ||
| 342 | settle 400 15000 | ||
| 343 | send printf 'm2a-%s\\n' pin\n | ||
| 344 | expect m2a-pin 15000 | ||
| 345 | settle 400 15000 | ||
| 346 | send \x1cc | ||
| 347 | expect \x1b[2J 15000 | ||
| 348 | settle 400 15000 | ||
| 349 | send printf 'm2b-%s\\n' pin\n | ||
| 350 | expect m2b-pin 15000 | ||
| 351 | settle 400 15000 | ||
| 352 | send \x1cc | ||
| 353 | expect \x1b[2J 15000 | ||
| 354 | settle 400 15000 | ||
| 355 | send \x1cc | ||
| 356 | expect \x1b[2J 15000 | ||
| 357 | settle 400 15000 | ||
| 358 | send printf 'm2c-%s\\n' pin\n | ||
| 359 | expect m2c-pin 15000 | ||
| 360 | settle 400 15000 | ||
| 361 | send \x1cc | ||
| 362 | expect m2c-pin 15000 | ||
| 363 | settle 400 15000 | ||
| 364 | send printf 'm2d-%s\\n' pin\n | ||
| 365 | expect m2d-pin 15000 | ||
| 366 | settle 400 15000 | ||
| 367 | send \x1c\x1c | ||
| 368 | waitexit 10000 | ||
| 369 | EOF | ||
| 370 | RC=$? | ||
| 371 | set -e | ||
| 372 | [ "$RC" -eq 0 ] || { | ||
| 373 | echo "e2e FAIL: new session: ptyclient leg exited $RC (did \\x1cc switch?):" | ||
| 374 | cat "$OUT.nsw.log"; exit 1; } | ||
| 375 | "$MUXA" status --sock "$SOCK28" --session 1 > "$OUT.nswst" 2>&1 || { | ||
| 376 | echo "e2e FAIL: new session: the chord did not create session 1:" | ||
| 377 | cat "$OUT.nswst"; exit 1; } | ||
| 378 | # The session the client LEFT: still live, still holding what it was shown | ||
| 379 | # before the chord, and innocent of everything typed after it. | ||
| 380 | "$MUXA" capture --sock "$SOCK28" > "$OUT.nswcap" 2>&1 | ||
| 381 | grep -q "m2a-pin" "$OUT.nswcap" || { | ||
| 382 | echo "e2e FAIL: new session: the default session lost its own marker:" | ||
| 383 | cat "$OUT.nswcap"; exit 1; } | ||
| 384 | grep -q "m2b-pin" "$OUT.nswcap" && { | ||
| 385 | echo "e2e FAIL: new session: the second marker ran in the OLD session" | ||
| 386 | cat "$OUT.nswcap"; exit 1; } | ||
| 387 | # The fourth chord had nowhere to go: the fill above took 28 slots and the | ||
| 388 | # chords the other four (0, 1, 2, 3), so the daemon answered the attach with an | ||
| 389 | # exit_status before a single frame of state. That refusal is NOT fatal — | ||
| 390 | # the tile the chord created knows which tile it was born from — and the | ||
| 391 | # three claims below are what "fell back" means, none of which the log | ||
| 392 | # shows on its own. | ||
| 393 | # | ||
| 394 | # It is also why the fourth chord's wait is `expect m2c-pin` and not the | ||
| 395 | # `[2J` the other three use: a refused tile's screen is cleared like any | ||
| 396 | # other, so `[2J` cannot tell "the zoom arrived" from "the zoom came | ||
| 397 | # straight back". Session 3's own marker, repainted from a replica that was | ||
| 398 | # hot the whole time, can — and it is the same positive the capture | ||
| 399 | # assertion below leans on. | ||
| 400 | # | ||
| 401 | # * the client said so, once, in its own words. Its stderr is ptyclient's | ||
| 402 | # --err file, not the pty, so the message is read there. | ||
| 403 | grep -q "cannot create a new session" "$OUT.nsw.err" || { | ||
| 404 | echo "e2e FAIL: new session: the refused switch said nothing:" | ||
| 405 | cat "$OUT.nsw.err"; exit 1; } | ||
| 406 | # * it is back in session 3, the one it typed the chord in — proved | ||
| 407 | # POSITIVELY, by a marker run AFTER the refusal landing in that | ||
| 408 | # session's grid beside the one from before it. A capture that held | ||
| 409 | # only m2c-pin would equally describe a client that died there. | ||
| 410 | "$MUXA" capture --sock "$SOCK28" --session 3 > "$OUT.nswcap3" 2>&1 | ||
| 411 | grep -q "m2c-pin" "$OUT.nswcap3" || { | ||
| 412 | echo "e2e FAIL: new session: session 3 lost the marker from before the refusal:" | ||
| 413 | cat "$OUT.nswcap3"; exit 1; } | ||
| 414 | grep -q "m2d-pin" "$OUT.nswcap3" || { | ||
| 415 | echo "e2e FAIL: new session: the refused switch did not come back to session 3:" | ||
| 416 | cat "$OUT.nswcap3"; exit 1; } | ||
| 417 | # * and the name it was reaching for was never created. Without this the | ||
| 418 | # leg would pass on a daemon that quietly grew one more slot. | ||
| 419 | "$MUXA" status --sock "$SOCK28" --session 4 > "$OUT.nswst4" 2>&1 && { | ||
| 420 | echo "e2e FAIL: new session: the daemon created a 33rd session:" | ||
| 421 | cat "$OUT.nswst4"; exit 1; } | ||
| 422 | assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop" | ||
| 423 | D25PID="" | ||
| 424 | ok "Ctrl-\\ c: a new session is created and switched to, the old one intact; a full daemon refuses and the client stays where it was" | ||
| 425 | |||
| 426 | # ---- M3: Ctrl-\ n / Ctrl-\ p step around the session ring ---------------- | ||
| 427 | # | ||
| 428 | # THREE sessions, not two, and that is the whole point: with two, next and | ||
| 429 | # previous land on the same place and the leg would pass with the two | ||
| 430 | # directions swapped. Three makes a wrong direction land on a different | ||
| 431 | # marker. | ||
| 432 | # | ||
| 433 | # Each session is given its own marker before the stepping starts, so the | ||
| 434 | # ring's answer is read off the SCREEN rather than off a name: after a | ||
| 435 | # step, the marker that arrives says which grid the daemon just sent. | ||
| 436 | # | ||
| 437 | # * `\x1cn` from the last slot must wrap to the first — m3a-pin, the | ||
| 438 | # default session's marker. A step in the other direction would bring | ||
| 439 | # m3b-pin, and the expect would time out on it. | ||
| 440 | # * `\x1cp` from the first slot must wrap back to the last — m3c-pin. | ||
| 441 | # | ||
| 442 | # The markers arrive a second time because arriving somewhere repaints that | ||
| 443 | # session's grid; ptyclient's expect cursor consumes matches, so the m3a-pin | ||
| 444 | # the leg waited for at the top cannot satisfy the one it waits for after | ||
| 445 | # the wrap. | ||
| 446 | # | ||
| 447 | # PHASE 3C changed what a step COSTS, not where it lands. `n` used to detach, | ||
| 448 | # re-dial and re-attach, and the leg waited for the new client's `?1049h`. | ||
| 449 | # Now the ring moves the ZOOM: sessions 0, 1 and 2 all have tiles (the `c` | ||
| 450 | # chords made them), their replicas are hot, and the step is a local repaint | ||
| 451 | # at zero round trips. The alternate screen is entered once and never left, | ||
| 452 | # so the arrival signal is `setFocus`'s screen clear followed by the marker | ||
| 453 | # the repaint carries — and the marker is what this leg was always really | ||
| 454 | # reading the answer off. | ||
| 455 | # | ||
| 456 | # The captures afterwards pin which marker lives where — three sessions | ||
| 457 | # that each kept their own line, so the stepping moved the CLIENT and left | ||
| 458 | # the sessions where they were. | ||
| 459 | start_daemon "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound" --shell /bin/sh | ||
| 460 | D26PID=$DPID | ||
| 461 | |||
| 462 | set +e | ||
| 463 | # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48) | ||
| 464 | timeout 60 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.ring" --err "$OUT.ring.err" \ | ||
| 465 | -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF' | ||
| 466 | expect \x1b[?1049h 15000 | ||
| 467 | settle 400 15000 | ||
| 468 | send printf 'm3a-%s\\n' pin\n | ||
| 469 | expect m3a-pin 15000 | ||
| 470 | settle 400 15000 | ||
| 471 | send \x1cc | ||
| 472 | expect \x1b[2J 15000 | ||
| 473 | settle 400 15000 | ||
| 474 | send printf 'm3b-%s\\n' pin\n | ||
| 475 | expect m3b-pin 15000 | ||
| 476 | settle 400 15000 | ||
| 477 | send \x1cc | ||
| 478 | expect \x1b[2J 15000 | ||
| 479 | settle 400 15000 | ||
| 480 | send printf 'm3c-%s\\n' pin\n | ||
| 481 | expect m3c-pin 15000 | ||
| 482 | settle 400 15000 | ||
| 483 | send \x1cn | ||
| 484 | expect m3a-pin 15000 | ||
| 485 | settle 400 15000 | ||
| 486 | send \x1cp | ||
| 487 | expect m3c-pin 15000 | ||
| 488 | settle 400 15000 | ||
| 489 | send \x1c\x1c | ||
| 490 | waitexit 10000 | ||
| 491 | EOF | ||
| 492 | RC=$? | ||
| 493 | set -e | ||
| 494 | [ "$RC" -eq 0 ] || { | ||
| 495 | echo "e2e FAIL: session ring: ptyclient leg exited $RC (did \\x1cn wrap?):" | ||
| 496 | cat "$OUT.ring.log"; exit 1; } | ||
| 497 | for s in 0:m3a 1:m3b 2:m3c; do | ||
| 498 | _sess="${s%%:*}"; _mark="${s##*:}-pin" | ||
| 499 | timeout 20 "$MUXA" capture --sock "$SOCK29" --session "$_sess" > "$OUT.ringcap" 2>&1 | ||
| 500 | grep -q "$_mark" "$OUT.ringcap" || { | ||
| 501 | echo "e2e FAIL: session ring: session $_sess does not hold $_mark:" | ||
| 502 | cat "$OUT.ringcap"; exit 1; } | ||
| 503 | # Its neighbours' markers are not in it: the client moved between | ||
| 504 | # sessions, it did not drag one session's shell along behind it. | ||
| 505 | grep -v "$_mark" "$OUT.ringcap" | grep -qE 'm3[abc]-pin' && { | ||
| 506 | echo "e2e FAIL: session ring: session $_sess holds another session's marker:" | ||
| 507 | cat "$OUT.ringcap"; exit 1; } | ||
| 508 | done | ||
| 509 | assert_stopped "$SOCK29" "$D26PID" "session ring" "$OUT.ringstop" | ||
| 510 | D26PID="" | ||
| 511 | ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends" | ||
| 512 | |||
| 513 | # ---- M5: a session shell knows where it is, and mux refuses the self-loop -- | ||
| 514 | # | ||
| 515 | # The incident: `mux --sock S` typed in a shell of session 0 on S attached | ||
| 516 | # that session to itself. Paint became a delta became a repaint, the inner | ||
| 517 | # client took the alternate screen, and it ate every keystroke — and since | ||
| 518 | # Ctrl-\ became a prefix the OUTER keyboard cannot steer an inner client | ||
| 519 | # back out. There is no escape chord to offer, so the loop is refused. | ||
| 520 | # | ||
| 521 | # Three claims, in the order that makes each one's failure legible: | ||
| 522 | # | ||
| 523 | # 1. The daemon's identity reached a REAL shell. The SHELL does the | ||
| 524 | # comparing — `[ "$MUX_SOCK" = ... ]` — so a pass means the child saw | ||
| 525 | # the path this block bound, not that the script can rebuild a string. | ||
| 526 | # 2. The refusal fires on the self-pair, and the loop never starts. The | ||
| 527 | # money assertion is `"alt_screen":false` AFTER the injection: before | ||
| 528 | # this milestone that read true, because an inner client really had | ||
| 529 | # taken the screen. A message grep alone would still pass if the client | ||
| 530 | # printed the line and attached anyway. | ||
| 531 | # 3. `mux wall` refuses a tile that is the session it is running in, by | ||
| 532 | # name. A wall that silently dropped it would be a wall that lies. | ||
| 533 | # | ||
| 534 | # Every marker is assembled by printf from pieces and never typed whole, so | ||
| 535 | # the grid's echo of the command line can never satisfy the grep. `; echo` | ||
| 536 | # rather than a `\n` inside printf's format: muxa's send turns every `\n` | ||
| 537 | # into a real newline, so a format string carrying one would put a shell | ||
| 538 | # continuation prompt in the middle of the assertion. | ||
| 539 | # | ||
| 540 | # No ptyclient here on purpose — a real client on a real pty is what the | ||
| 541 | # refusal PREVENTS, so the leg that injects it into the session shell is the | ||
| 542 | # leg that tests it. This block therefore needs no expect timing at all. | ||
| 543 | # | ||
| 544 | # 200 columns, and no client ever attaches to narrow it: every assertion | ||
| 545 | # here greps a MESSAGE out of a grid dump, and a grid wraps. At 80 the wall | ||
| 546 | # refusal — which names the spelling, so its length follows $TMPDIR — split | ||
| 547 | # mid-sentence and a correct refusal read as a missing one. Width is the fix | ||
| 548 | # that does not make the needle shorter than the claim. | ||
| 549 | MUXABS=$(cd "$(dirname "$MUX")" && pwd)/$(basename "$MUX") | ||
| 550 | start_daemon "$SOCK31" "$OUT.sa.d" "self-attach daemon never bound" --shell /bin/sh --cols 200 | ||
| 551 | D28PID=$DPID | ||
| 552 | |||
| 553 | timeout 20 "$MUXA" send \ | ||
| 554 | "[ \"\$MUX_SOCK\" = \"$SOCK31\" ] && printf 'ENV%s-%s' OK \"\$MUX_SESSION\"; echo\n" \ | ||
| 555 | --sock "$SOCK31" --session 0 > "$OUT.saenv" 2>&1 | ||
| 556 | sleep 1 | ||
| 557 | timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap1" 2>&1 | ||
| 558 | # ENVOK-0 is both halves at once: the socket matched, and the name the shell | ||
| 559 | # was told is the RESOLVED default, not the empty spelling the wire uses. | ||
| 560 | grep -q "ENVOK-0" "$OUT.sacap1" || { | ||
| 561 | echo "e2e FAIL: self-attach: the session shell was not told MUX_SOCK=$SOCK31 and MUX_SESSION=0:" | ||
| 562 | cat "$OUT.saenv"; cat "$OUT.sacap1"; exit 1; } | ||
| 563 | |||
| 564 | timeout 20 "$MUXA" send \ | ||
| 565 | "$MUXABS --sock $SOCK31; printf 'SELF%s-%s' rc \$?; echo\n" \ | ||
| 566 | --sock "$SOCK31" --session 0 > "$OUT.saself" 2>&1 | ||
| 567 | sleep 2 | ||
| 568 | timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap2" 2>&1 | ||
| 569 | grep -q "unset MUX_SESSION to override" "$OUT.sacap2" || { | ||
| 570 | echo "e2e FAIL: self-attach: mux did not refuse the session it was running in:" | ||
| 571 | cat "$OUT.saself"; cat "$OUT.sacap2"; exit 1; } | ||
| 572 | # Exit 2, the usage-error code every other refusal in this binary uses, and | ||
| 573 | # proof the process ENDED rather than sitting there attached. | ||
| 574 | grep -q "SELFrc-2" "$OUT.sacap2" || { | ||
| 575 | echo "e2e FAIL: self-attach: the refusal did not exit 2 (or did not exit):" | ||
| 576 | cat "$OUT.sacap2"; exit 1; } | ||
| 577 | timeout 20 "$MUXA" status --sock "$SOCK31" --session 0 > "$OUT.sast" 2>&1 | ||
| 578 | grep -q '"alt_screen":false' "$OUT.sast" || { | ||
| 579 | echo "e2e FAIL: self-attach: an inner client took the alternate screen — the loop ran:" | ||
| 580 | cat "$OUT.sast"; exit 1; } | ||
| 581 | |||
| 582 | # The wall's spelling is ONE argument, quotes and all (the wall grammar, | ||
| 583 | # wall.zig) — the same string scenario 33 puts in the state file. | ||
| 584 | timeout 20 "$MUXA" send \ | ||
| 585 | "$MUXABS wall '--sock $SOCK31#0'; printf 'WALL%s-%s' rc \$?; echo\n" \ | ||
| 586 | --sock "$SOCK31" --session 0 > "$OUT.sawall" 2>&1 | ||
| 587 | sleep 2 | ||
| 588 | timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap3" 2>&1 | ||
| 589 | grep -q -- "mux: wall target '--sock $SOCK31#0' is the session this shell is inside" "$OUT.sacap3" || { | ||
| 590 | echo "e2e FAIL: self-attach: mux wall struck its own tile without naming it:" | ||
| 591 | cat "$OUT.sawall"; cat "$OUT.sacap3"; exit 1; } | ||
| 592 | grep -q "WALLrc-2" "$OUT.sacap3" || { | ||
| 593 | echo "e2e FAIL: self-attach: the wall refusal did not exit 2 (or did not exit):" | ||
| 594 | cat "$OUT.sacap3"; exit 1; } | ||
| 595 | assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop" | ||
| 596 | D28PID="" | ||
| 597 | ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" | ||
| 598 | |||
| 599 | # The wheel pair. Each needs a scrollback of its own to scroll (or to prove | 148 | # The wheel pair. Each needs a scrollback of its own to scroll (or to prove |
| 600 | # it did not), which the long-lived /bin/sh daemon has no way to hold still, | 149 | # it did not), which the long-lived /bin/sh daemon has no way to hold still, |
| 601 | # and the second one needs a session that ASKS for the mouse — a mode that | 150 | # and the second one needs a session that ASKS for the mouse — a mode that |
test/e2e_07_wallcli.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,454 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_07_wallcli.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 CLI wall (`mux wall`): its own daemon, so its two sessions can't be | ||
| 6 | # confused with any other block's. | ||
| 7 | SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock" | ||
| 8 | defer_sock "$SOCK26" | ||
| 9 | # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord | ||
| 10 | # that fails to detach cannot strand another block's session. | ||
| 11 | SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" | ||
| 12 | defer_sock "$SOCK27" | ||
| 13 | # Ctrl-\ c (create a session and switch to it): its own daemon, because the | ||
| 14 | # assertion is about which sessions EXIST — another block's session on the | ||
| 15 | # same daemon would change the name the chord picks. | ||
| 16 | SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock" | ||
| 17 | defer_sock "$SOCK28" | ||
| 18 | # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same | ||
| 19 | # reason as SOCK28, and more sharply — the ring IS the list of sessions on | ||
| 20 | # one daemon, so a stray session from another block would change where a | ||
| 21 | # step lands. | ||
| 22 | SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" | ||
| 23 | defer_sock "$SOCK29" | ||
| 24 | # M5 (the self-attach refusal): its own daemon because the assertion reads | ||
| 25 | # the SOCKET PATH out of a session shell's environment and compares it to | ||
| 26 | # the one this block started — a shared daemon would make the comparison a | ||
| 27 | # tautology about whichever block bound first. | ||
| 28 | SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock" | ||
| 29 | defer_sock "$SOCK31" | ||
| 30 | |||
| 31 | # ---- the CLI wall: the hub's multiattach, in one terminal -------------- | ||
| 32 | # One daemon, two named sessions, one `mux wall` on a real pty showing | ||
| 33 | # both AT ONCE — the browser wall's claim, made by the CLI. Three facts, | ||
| 34 | # each with its own witness: both sessions' markers painted (the snapshot | ||
| 35 | # path), a line injected into b WHILE the wall is attached painted too | ||
| 36 | # (the delta path — the wall is a live replica, not a capture), and | ||
| 37 | # `\x1cd` ended the process with exit 0 (the wall left, not died). Markers | ||
| 38 | # are shell-EXPANDED (`%s`+pin), the M18 trick: a hit is the shell's work, | ||
| 39 | # never an echo of anything typed here. | ||
| 40 | # | ||
| 41 | # The one expect anchors on the injected delta, deliberately: tile pumps | ||
| 42 | # paint their first stripes in whichever order their snapshots land — a | ||
| 43 | # thread race, and ptyclient's expect cursor cannot bet on it. The | ||
| 44 | # injected delta is the one byte sequence guaranteed late (t+5s into a | ||
| 45 | # replica that must already hold b's snapshot); the snapshot markers are | ||
| 46 | # asserted order-free by the greps on the capture below. | ||
| 47 | # | ||
| 48 | # The two tiles are spelled DIFFERENTLY on purpose: `--sock PATH` as two | ||
| 49 | # arguments (muxweb's dialect) and as one quoted spelling (the wall file's). | ||
| 50 | # Both must reach the same tile, so both are pinned by this one leg. | ||
| 51 | start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh | ||
| 52 | D23PID=$DPID | ||
| 53 | |||
| 54 | pipe_mux "$OUT.cwa" "$OUT.cwa.err" timeout 40 "$MUX" --sock "$SOCK26" --session a | ||
| 55 | pipe_send 'printf "cwa-%%s\\n" pin\n' | ||
| 56 | await_out "$OUT.cwa" "cwa-pin" "cwa-pin never reached the client" | ||
| 57 | pipe_detach | ||
| 58 | wait_grid "$SOCK26" "cwa-pin" "CLI wall: session a's marker" a | ||
| 59 | pipe_mux "$OUT.cwb" "$OUT.cwb.err" timeout 40 "$MUX" --sock "$SOCK26" --session b | ||
| 60 | pipe_send 'printf "cwb-%%s\\n" pin\n' | ||
| 61 | await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client" | ||
| 62 | pipe_detach | ||
| 63 | wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b | ||
| 64 | |||
| 65 | ( sleep 5; "$MUXA" send 'printf "cwlive-%s\n" pin\n' \ | ||
| 66 | --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) & | ||
| 67 | CWINJPID=$! | ||
| 68 | defer_kill "$CWINJPID" | ||
| 69 | set +e | ||
| 70 | timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \ | ||
| 71 | "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF' | ||
| 72 | expect cwlive-pin 20000 | ||
| 73 | send \x1cd | ||
| 74 | waitexit 10000 | ||
| 75 | EOF | ||
| 76 | RC=$? | ||
| 77 | set -e | ||
| 78 | wait "$CWINJPID" 2>/dev/null || true | ||
| 79 | [ "$RC" -eq 0 ] || { | ||
| 80 | echo "e2e FAIL: CLI wall: ptyclient leg exited $RC:" | ||
| 81 | cat "$OUT.cwpc"; cat "$OUT.cwinj"; exit 1; } | ||
| 82 | # The multiattach fact itself: both sessions' bytes in ONE process's | ||
| 83 | # capture. Redundant with the expects above only until someone edits the | ||
| 84 | # script block; a grep each keeps the claim standing on its own. | ||
| 85 | grep -q "cwa-pin" "$OUT.cwcap" || { | ||
| 86 | echo "e2e FAIL: CLI wall: a's marker never painted"; exit 1; } | ||
| 87 | grep -q "cwb-pin" "$OUT.cwcap" || { | ||
| 88 | echo "e2e FAIL: CLI wall: b's marker never painted"; exit 1; } | ||
| 89 | grep -q "cwlive-pin" "$OUT.cwcap" || { | ||
| 90 | echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; } | ||
| 91 | # Every tile claims its rect: a and b were created 80-wide by pipe, and | ||
| 92 | # this 40-wide wall resized both on attach. There is no 0x0 passivity to | ||
| 93 | # assert — the wall writes the sessions it shows. | ||
| 94 | assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop" | ||
| 95 | D23PID="" | ||
| 96 | ok "mux wall: two sessions and a live delta on one terminal" | ||
| 97 | |||
| 98 | # ---- M1: Ctrl-\ is a prefix, not an action ------------------------------ | ||
| 99 | # | ||
| 100 | # Two claims in one client, because they are the same claim seen from both | ||
| 101 | # sides: the chord layer eats exactly the two bytes of a chord and nothing | ||
| 102 | # else. | ||
| 103 | # | ||
| 104 | # (a) an unknown command key is swallowed WITH its prefix. `\x1cz` is sent | ||
| 105 | # at an empty prompt (`z` is bound to nothing; `x` would forget the | ||
| 106 | # tile), then a marker command. If either byte had reached the pty the | ||
| 107 | # shell would have read `zprintf ...` and the marker would never | ||
| 108 | # print — so the marker is the assertion, and the explicit grep for | ||
| 109 | # `zprintf` names what went wrong when it does. | ||
| 110 | # (b) `\x1cd` detaches. The client exits 0 and the session it left behind | ||
| 111 | # still answers, which is what separates a detach from a kill. | ||
| 112 | # | ||
| 113 | # A real pty (ptyclient) rather than a pipeline: the shell only echoes what | ||
| 114 | # it was really given when there is a terminal to echo to, and the echo is | ||
| 115 | # what carries the stray `z` if the layer leaks one. | ||
| 116 | start_daemon "$SOCK27" "$OUT.pfx.d" "prefix daemon never bound" --shell /bin/sh | ||
| 117 | D24PID=$DPID | ||
| 118 | |||
| 119 | set +e | ||
| 120 | timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pfx" --err "$OUT.pfx.err" \ | ||
| 121 | -- "$MUX" --sock "$SOCK27" > "$OUT.pfx.log" 2>&1 <<'EOF' | ||
| 122 | expect \x1b[?1049h 15000 | ||
| 123 | settle 400 15000 | ||
| 124 | send \x1cz | ||
| 125 | settle 400 15000 | ||
| 126 | send printf 'm1-%s\\n' pin\n | ||
| 127 | expect m1-pin 15000 | ||
| 128 | settle 400 15000 | ||
| 129 | send \x1cd | ||
| 130 | waitexit 10000 | ||
| 131 | EOF | ||
| 132 | RC=$? | ||
| 133 | set -e | ||
| 134 | [ "$RC" -eq 0 ] || { | ||
| 135 | echo "e2e FAIL: prefix: ptyclient leg exited $RC (did \\x1cd detach?):" | ||
| 136 | cat "$OUT.pfx.log"; exit 1; } | ||
| 137 | grep -q "m1-pin" "$OUT.pfx" || { | ||
| 138 | echo "e2e FAIL: prefix: the marker never printed"; exit 1; } | ||
| 139 | grep -q "zprintf" "$OUT.pfx" && { | ||
| 140 | echo "e2e FAIL: prefix: the unknown command key reached the pty" | ||
| 141 | cat "$OUT.pfx"; exit 1; } | ||
| 142 | # Detached, not exited: the session outlives the client that left it. | ||
| 143 | "$MUXA" status --sock "$SOCK27" > "$OUT.pfxst" 2>&1 || { | ||
| 144 | echo "e2e FAIL: prefix: the session did not survive the detach:" | ||
| 145 | cat "$OUT.pfxst"; exit 1; } | ||
| 146 | assert_stopped "$SOCK27" "$D24PID" "prefix" "$OUT.pfxstop" | ||
| 147 | D24PID="" | ||
| 148 | ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches" | ||
| 149 | |||
| 150 | # ---- M2: Ctrl-\ c creates a session and switches to it ------------------- | ||
| 151 | # | ||
| 152 | # The claim is a MOVE, which takes two witnesses: the client ends up | ||
| 153 | # somewhere new, and the place it left is still there and unchanged. | ||
| 154 | # | ||
| 155 | # * one pty client attaches to the default session, runs a marker, types | ||
| 156 | # `\x1cc`, and runs a second marker. Both markers reach a shell — the | ||
| 157 | # second one proves the client is attached to a session that works, not | ||
| 158 | # merely alive after the chord. | ||
| 159 | # * `muxa status --session 1` proves the daemon really created the name | ||
| 160 | # the client picked (lowest free integer beside the default "0"). | ||
| 161 | # * the default session's capture still holds the FIRST marker and not the | ||
| 162 | # second. Without that pair the same log would be produced by a chord | ||
| 163 | # that did nothing at all. | ||
| 164 | # | ||
| 165 | # The leg then keeps pressing until the daemon runs out of sessions, because | ||
| 166 | # the refusal is the branch with the interesting answer: a refused attach is | ||
| 167 | # the one failure the zoom recovers from instead of exiting, and nothing | ||
| 168 | # else in the suite reaches it. See the block after the leg for the three | ||
| 169 | # things "recovered" has to mean. | ||
| 170 | # | ||
| 171 | # PHASE 3C evolved the WAIT, and only the wait. `\x1cc` used to tear the | ||
| 172 | # terminal down and build it back up — a fresh `?1049h` per switch — because | ||
| 173 | # each switch was a new client process on a new transport. Now `mux` IS the | ||
| 174 | # wall: the chord adds a tile and moves the ZOOM to it, and the alternate | ||
| 175 | # screen was entered once, at startup, and is never left. So the arrival | ||
| 176 | # signal is the zoom's own: `setFocus` clears the screen (`\x1b[2J`) on the | ||
| 177 | # thread that moves it, before the terminal changes hands. | ||
| 178 | # | ||
| 179 | # That needle is not merely "some paint": between the chord and the answer | ||
| 180 | # the old tile is idle and paints nothing, so the next `[2J` after the chord | ||
| 181 | # is the handover itself. Waiting for it MATTERS — keystrokes sent before it | ||
| 182 | # would go to the mailbox of the tile the user is leaving, which is the | ||
| 183 | # session they were typing in a moment ago and would silently swallow the | ||
| 184 | # marker. | ||
| 185 | start_daemon "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" --shell /bin/sh | ||
| 186 | D25PID=$DPID | ||
| 187 | # The refusal this leg's fourth chord asserts needs a FULL table. The | ||
| 188 | # chords make four (0, 1, 2, 3); the other 28 are filled here. `fillN` is | ||
| 189 | # not an integer, so nextFreeName still hands the chords 1, 2, 3 and | ||
| 190 | # reaches for "4" on the refused one — which is the name the post-check | ||
| 191 | # below asks about. | ||
| 192 | fill_sessions "$SOCK28" "$OUT.nswfill" fill 4 31 | ||
| 193 | |||
| 194 | set +e | ||
| 195 | # tall: Ctrl-\ c adds a second tile at 80x24 (80 >= 48) | ||
| 196 | timeout 40 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.nsw" --err "$OUT.nsw.err" \ | ||
| 197 | -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF' | ||
| 198 | expect \x1b[?1049h 15000 | ||
| 199 | settle 400 15000 | ||
| 200 | send printf 'm2a-%s\\n' pin\n | ||
| 201 | expect m2a-pin 15000 | ||
| 202 | settle 400 15000 | ||
| 203 | send \x1cc | ||
| 204 | expect \x1b[2J 15000 | ||
| 205 | settle 400 15000 | ||
| 206 | send printf 'm2b-%s\\n' pin\n | ||
| 207 | expect m2b-pin 15000 | ||
| 208 | settle 400 15000 | ||
| 209 | send \x1cc | ||
| 210 | expect \x1b[2J 15000 | ||
| 211 | settle 400 15000 | ||
| 212 | send \x1cc | ||
| 213 | expect \x1b[2J 15000 | ||
| 214 | settle 400 15000 | ||
| 215 | send printf 'm2c-%s\\n' pin\n | ||
| 216 | expect m2c-pin 15000 | ||
| 217 | settle 400 15000 | ||
| 218 | send \x1cc | ||
| 219 | expect m2c-pin 15000 | ||
| 220 | settle 400 15000 | ||
| 221 | send printf 'm2d-%s\\n' pin\n | ||
| 222 | expect m2d-pin 15000 | ||
| 223 | settle 400 15000 | ||
| 224 | send \x1c\x1c | ||
| 225 | waitexit 10000 | ||
| 226 | EOF | ||
| 227 | RC=$? | ||
| 228 | set -e | ||
| 229 | [ "$RC" -eq 0 ] || { | ||
| 230 | echo "e2e FAIL: new session: ptyclient leg exited $RC (did \\x1cc switch?):" | ||
| 231 | cat "$OUT.nsw.log"; exit 1; } | ||
| 232 | "$MUXA" status --sock "$SOCK28" --session 1 > "$OUT.nswst" 2>&1 || { | ||
| 233 | echo "e2e FAIL: new session: the chord did not create session 1:" | ||
| 234 | cat "$OUT.nswst"; exit 1; } | ||
| 235 | # The session the client LEFT: still live, still holding what it was shown | ||
| 236 | # before the chord, and innocent of everything typed after it. | ||
| 237 | "$MUXA" capture --sock "$SOCK28" > "$OUT.nswcap" 2>&1 | ||
| 238 | grep -q "m2a-pin" "$OUT.nswcap" || { | ||
| 239 | echo "e2e FAIL: new session: the default session lost its own marker:" | ||
| 240 | cat "$OUT.nswcap"; exit 1; } | ||
| 241 | grep -q "m2b-pin" "$OUT.nswcap" && { | ||
| 242 | echo "e2e FAIL: new session: the second marker ran in the OLD session" | ||
| 243 | cat "$OUT.nswcap"; exit 1; } | ||
| 244 | # The fourth chord had nowhere to go: the fill above took 28 slots and the | ||
| 245 | # chords the other four (0, 1, 2, 3), so the daemon answered the attach with an | ||
| 246 | # exit_status before a single frame of state. That refusal is NOT fatal — | ||
| 247 | # the tile the chord created knows which tile it was born from — and the | ||
| 248 | # three claims below are what "fell back" means, none of which the log | ||
| 249 | # shows on its own. | ||
| 250 | # | ||
| 251 | # It is also why the fourth chord's wait is `expect m2c-pin` and not the | ||
| 252 | # `[2J` the other three use: a refused tile's screen is cleared like any | ||
| 253 | # other, so `[2J` cannot tell "the zoom arrived" from "the zoom came | ||
| 254 | # straight back". Session 3's own marker, repainted from a replica that was | ||
| 255 | # hot the whole time, can — and it is the same positive the capture | ||
| 256 | # assertion below leans on. | ||
| 257 | # | ||
| 258 | # * the client said so, once, in its own words. Its stderr is ptyclient's | ||
| 259 | # --err file, not the pty, so the message is read there. | ||
| 260 | grep -q "cannot create a new session" "$OUT.nsw.err" || { | ||
| 261 | echo "e2e FAIL: new session: the refused switch said nothing:" | ||
| 262 | cat "$OUT.nsw.err"; exit 1; } | ||
| 263 | # * it is back in session 3, the one it typed the chord in — proved | ||
| 264 | # POSITIVELY, by a marker run AFTER the refusal landing in that | ||
| 265 | # session's grid beside the one from before it. A capture that held | ||
| 266 | # only m2c-pin would equally describe a client that died there. | ||
| 267 | "$MUXA" capture --sock "$SOCK28" --session 3 > "$OUT.nswcap3" 2>&1 | ||
| 268 | grep -q "m2c-pin" "$OUT.nswcap3" || { | ||
| 269 | echo "e2e FAIL: new session: session 3 lost the marker from before the refusal:" | ||
| 270 | cat "$OUT.nswcap3"; exit 1; } | ||
| 271 | grep -q "m2d-pin" "$OUT.nswcap3" || { | ||
| 272 | echo "e2e FAIL: new session: the refused switch did not come back to session 3:" | ||
| 273 | cat "$OUT.nswcap3"; exit 1; } | ||
| 274 | # * and the name it was reaching for was never created. Without this the | ||
| 275 | # leg would pass on a daemon that quietly grew one more slot. | ||
| 276 | "$MUXA" status --sock "$SOCK28" --session 4 > "$OUT.nswst4" 2>&1 && { | ||
| 277 | echo "e2e FAIL: new session: the daemon created a 33rd session:" | ||
| 278 | cat "$OUT.nswst4"; exit 1; } | ||
| 279 | assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop" | ||
| 280 | D25PID="" | ||
| 281 | ok "Ctrl-\\ c: a new session is created and switched to, the old one intact; a full daemon refuses and the client stays where it was" | ||
| 282 | |||
| 283 | # ---- M3: Ctrl-\ n / Ctrl-\ p step around the session ring ---------------- | ||
| 284 | # | ||
| 285 | # THREE sessions, not two, and that is the whole point: with two, next and | ||
| 286 | # previous land on the same place and the leg would pass with the two | ||
| 287 | # directions swapped. Three makes a wrong direction land on a different | ||
| 288 | # marker. | ||
| 289 | # | ||
| 290 | # Each session is given its own marker before the stepping starts, so the | ||
| 291 | # ring's answer is read off the SCREEN rather than off a name: after a | ||
| 292 | # step, the marker that arrives says which grid the daemon just sent. | ||
| 293 | # | ||
| 294 | # * `\x1cn` from the last slot must wrap to the first — m3a-pin, the | ||
| 295 | # default session's marker. A step in the other direction would bring | ||
| 296 | # m3b-pin, and the expect would time out on it. | ||
| 297 | # * `\x1cp` from the first slot must wrap back to the last — m3c-pin. | ||
| 298 | # | ||
| 299 | # The markers arrive a second time because arriving somewhere repaints that | ||
| 300 | # session's grid; ptyclient's expect cursor consumes matches, so the m3a-pin | ||
| 301 | # the leg waited for at the top cannot satisfy the one it waits for after | ||
| 302 | # the wrap. | ||
| 303 | # | ||
| 304 | # PHASE 3C changed what a step COSTS, not where it lands. `n` used to detach, | ||
| 305 | # re-dial and re-attach, and the leg waited for the new client's `?1049h`. | ||
| 306 | # Now the ring moves the ZOOM: sessions 0, 1 and 2 all have tiles (the `c` | ||
| 307 | # chords made them), their replicas are hot, and the step is a local repaint | ||
| 308 | # at zero round trips. The alternate screen is entered once and never left, | ||
| 309 | # so the arrival signal is `setFocus`'s screen clear followed by the marker | ||
| 310 | # the repaint carries — and the marker is what this leg was always really | ||
| 311 | # reading the answer off. | ||
| 312 | # | ||
| 313 | # The captures afterwards pin which marker lives where — three sessions | ||
| 314 | # that each kept their own line, so the stepping moved the CLIENT and left | ||
| 315 | # the sessions where they were. | ||
| 316 | start_daemon "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound" --shell /bin/sh | ||
| 317 | D26PID=$DPID | ||
| 318 | |||
| 319 | set +e | ||
| 320 | # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48) | ||
| 321 | timeout 60 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.ring" --err "$OUT.ring.err" \ | ||
| 322 | -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF' | ||
| 323 | expect \x1b[?1049h 15000 | ||
| 324 | settle 400 15000 | ||
| 325 | send printf 'm3a-%s\\n' pin\n | ||
| 326 | expect m3a-pin 15000 | ||
| 327 | settle 400 15000 | ||
| 328 | send \x1cc | ||
| 329 | expect \x1b[2J 15000 | ||
| 330 | settle 400 15000 | ||
| 331 | send printf 'm3b-%s\\n' pin\n | ||
| 332 | expect m3b-pin 15000 | ||
| 333 | settle 400 15000 | ||
| 334 | send \x1cc | ||
| 335 | expect \x1b[2J 15000 | ||
| 336 | settle 400 15000 | ||
| 337 | send printf 'm3c-%s\\n' pin\n | ||
| 338 | expect m3c-pin 15000 | ||
| 339 | settle 400 15000 | ||
| 340 | send \x1cn | ||
| 341 | expect m3a-pin 15000 | ||
| 342 | settle 400 15000 | ||
| 343 | send \x1cp | ||
| 344 | expect m3c-pin 15000 | ||
| 345 | settle 400 15000 | ||
| 346 | send \x1c\x1c | ||
| 347 | waitexit 10000 | ||
| 348 | EOF | ||
| 349 | RC=$? | ||
| 350 | set -e | ||
| 351 | [ "$RC" -eq 0 ] || { | ||
| 352 | echo "e2e FAIL: session ring: ptyclient leg exited $RC (did \\x1cn wrap?):" | ||
| 353 | cat "$OUT.ring.log"; exit 1; } | ||
| 354 | for s in 0:m3a 1:m3b 2:m3c; do | ||
| 355 | _sess="${s%%:*}"; _mark="${s##*:}-pin" | ||
| 356 | timeout 20 "$MUXA" capture --sock "$SOCK29" --session "$_sess" > "$OUT.ringcap" 2>&1 | ||
| 357 | grep -q "$_mark" "$OUT.ringcap" || { | ||
| 358 | echo "e2e FAIL: session ring: session $_sess does not hold $_mark:" | ||
| 359 | cat "$OUT.ringcap"; exit 1; } | ||
| 360 | # Its neighbours' markers are not in it: the client moved between | ||
| 361 | # sessions, it did not drag one session's shell along behind it. | ||
| 362 | grep -v "$_mark" "$OUT.ringcap" | grep -qE 'm3[abc]-pin' && { | ||
| 363 | echo "e2e FAIL: session ring: session $_sess holds another session's marker:" | ||
| 364 | cat "$OUT.ringcap"; exit 1; } | ||
| 365 | done | ||
| 366 | assert_stopped "$SOCK29" "$D26PID" "session ring" "$OUT.ringstop" | ||
| 367 | D26PID="" | ||
| 368 | ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends" | ||
| 369 | |||
| 370 | # ---- M5: a session shell knows where it is, and mux refuses the self-loop -- | ||
| 371 | # | ||
| 372 | # The incident: `mux --sock S` typed in a shell of session 0 on S attached | ||
| 373 | # that session to itself. Paint became a delta became a repaint, the inner | ||
| 374 | # client took the alternate screen, and it ate every keystroke — and since | ||
| 375 | # Ctrl-\ became a prefix the OUTER keyboard cannot steer an inner client | ||
| 376 | # back out. There is no escape chord to offer, so the loop is refused. | ||
| 377 | # | ||
| 378 | # Three claims, in the order that makes each one's failure legible: | ||
| 379 | # | ||
| 380 | # 1. The daemon's identity reached a REAL shell. The SHELL does the | ||
| 381 | # comparing — `[ "$MUX_SOCK" = ... ]` — so a pass means the child saw | ||
| 382 | # the path this block bound, not that the script can rebuild a string. | ||
| 383 | # 2. The refusal fires on the self-pair, and the loop never starts. The | ||
| 384 | # money assertion is `"alt_screen":false` AFTER the injection: before | ||
| 385 | # this milestone that read true, because an inner client really had | ||
| 386 | # taken the screen. A message grep alone would still pass if the client | ||
| 387 | # printed the line and attached anyway. | ||
| 388 | # 3. `mux wall` refuses a tile that is the session it is running in, by | ||
| 389 | # name. A wall that silently dropped it would be a wall that lies. | ||
| 390 | # | ||
| 391 | # Every marker is assembled by printf from pieces and never typed whole, so | ||
| 392 | # the grid's echo of the command line can never satisfy the grep. `; echo` | ||
| 393 | # rather than a `\n` inside printf's format: muxa's send turns every `\n` | ||
| 394 | # into a real newline, so a format string carrying one would put a shell | ||
| 395 | # continuation prompt in the middle of the assertion. | ||
| 396 | # | ||
| 397 | # No ptyclient here on purpose — a real client on a real pty is what the | ||
| 398 | # refusal PREVENTS, so the leg that injects it into the session shell is the | ||
| 399 | # leg that tests it. This block therefore needs no expect timing at all. | ||
| 400 | # | ||
| 401 | # 200 columns, and no client ever attaches to narrow it: every assertion | ||
| 402 | # here greps a MESSAGE out of a grid dump, and a grid wraps. At 80 the wall | ||
| 403 | # refusal — which names the spelling, so its length follows $TMPDIR — split | ||
| 404 | # mid-sentence and a correct refusal read as a missing one. Width is the fix | ||
| 405 | # that does not make the needle shorter than the claim. | ||
| 406 | MUXABS=$(cd "$(dirname "$MUX")" && pwd)/$(basename "$MUX") | ||
| 407 | start_daemon "$SOCK31" "$OUT.sa.d" "self-attach daemon never bound" --shell /bin/sh --cols 200 | ||
| 408 | D28PID=$DPID | ||
| 409 | |||
| 410 | timeout 20 "$MUXA" send \ | ||
| 411 | "[ \"\$MUX_SOCK\" = \"$SOCK31\" ] && printf 'ENV%s-%s' OK \"\$MUX_SESSION\"; echo\n" \ | ||
| 412 | --sock "$SOCK31" --session 0 > "$OUT.saenv" 2>&1 | ||
| 413 | sleep 1 | ||
| 414 | timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap1" 2>&1 | ||
| 415 | # ENVOK-0 is both halves at once: the socket matched, and the name the shell | ||
| 416 | # was told is the RESOLVED default, not the empty spelling the wire uses. | ||
| 417 | grep -q "ENVOK-0" "$OUT.sacap1" || { | ||
| 418 | echo "e2e FAIL: self-attach: the session shell was not told MUX_SOCK=$SOCK31 and MUX_SESSION=0:" | ||
| 419 | cat "$OUT.saenv"; cat "$OUT.sacap1"; exit 1; } | ||
| 420 | |||
| 421 | timeout 20 "$MUXA" send \ | ||
| 422 | "$MUXABS --sock $SOCK31; printf 'SELF%s-%s' rc \$?; echo\n" \ | ||
| 423 | --sock "$SOCK31" --session 0 > "$OUT.saself" 2>&1 | ||
| 424 | sleep 2 | ||
| 425 | timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap2" 2>&1 | ||
| 426 | grep -q "unset MUX_SESSION to override" "$OUT.sacap2" || { | ||
| 427 | echo "e2e FAIL: self-attach: mux did not refuse the session it was running in:" | ||
| 428 | cat "$OUT.saself"; cat "$OUT.sacap2"; exit 1; } | ||
| 429 | # Exit 2, the usage-error code every other refusal in this binary uses, and | ||
| 430 | # proof the process ENDED rather than sitting there attached. | ||
| 431 | grep -q "SELFrc-2" "$OUT.sacap2" || { | ||
| 432 | echo "e2e FAIL: self-attach: the refusal did not exit 2 (or did not exit):" | ||
| 433 | cat "$OUT.sacap2"; exit 1; } | ||
| 434 | timeout 20 "$MUXA" status --sock "$SOCK31" --session 0 > "$OUT.sast" 2>&1 | ||
| 435 | grep -q '"alt_screen":false' "$OUT.sast" || { | ||
| 436 | echo "e2e FAIL: self-attach: an inner client took the alternate screen — the loop ran:" | ||
| 437 | cat "$OUT.sast"; exit 1; } | ||
| 438 | |||
| 439 | # The wall's spelling is ONE argument, quotes and all (the wall grammar, | ||
| 440 | # wall.zig) — the same string scenario 33 puts in the state file. | ||
| 441 | timeout 20 "$MUXA" send \ | ||
| 442 | "$MUXABS wall '--sock $SOCK31#0'; printf 'WALL%s-%s' rc \$?; echo\n" \ | ||
| 443 | --sock "$SOCK31" --session 0 > "$OUT.sawall" 2>&1 | ||
| 444 | sleep 2 | ||
| 445 | timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap3" 2>&1 | ||
| 446 | grep -q -- "mux: wall target '--sock $SOCK31#0' is the session this shell is inside" "$OUT.sacap3" || { | ||
| 447 | echo "e2e FAIL: self-attach: mux wall struck its own tile without naming it:" | ||
| 448 | cat "$OUT.sawall"; cat "$OUT.sacap3"; exit 1; } | ||
| 449 | grep -q "WALLrc-2" "$OUT.sacap3" || { | ||
| 450 | echo "e2e FAIL: self-attach: the wall refusal did not exit 2 (or did not exit):" | ||
| 451 | cat "$OUT.sacap3"; exit 1; } | ||
| 452 | assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop" | ||
| 453 | D28PID="" | ||
| 454 | ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" | ||