a73x

fc698819

test: 07_wallcli moves to a file of its own

a73x   2026-08-26 19:00

Commit message
test: 07_wallcli moves to a file of its own

`mux wall` in one terminal, and the Ctrl-\ chords — 5 scenarios, 454 lines. The legs move unedited; the runner
sources the file where they used to stand.

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

test/e2e.sh
Old New
@@ -102,7 +102,7 @@ done
102 E2E_DIR=$(dirname "$0") 102 E2E_DIR=$(dirname "$0")
103 . "$E2E_DIR/e2e_lib.sh" 103 . "$E2E_DIR/e2e_lib.sh"
104 104
105 E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web' 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"