a73x

76778c7a

refactor: a daemon's declaration stands with the leg that starts it

a73x   2026-08-26 18:42

Commit message
refactor: a daemon's declaration stands with the leg that starts it

445 lines of socket paths, keys, ports and state homes sat at the top of
the file, hundreds of lines from the scenarios that use them, because the
old trap named every one of them and `set -u` needed them bound before it
could. The trap reads a register now, so the reason is gone and the
declarations go back to their legs — the prose explaining WHY each daemon
is separate is worth most beside the daemon it argues for.

Two blocks move for the same reason. The long-lived daemon's teardown was
4,000 lines below its last user, in the middle of the pane legs; it now
stands where that user ends. The prose about the count pins was in the
same place, describing two literals at the far end of the file; it moves
to them.

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

test/e2e.sh
Old New
@@ -237,467 +237,6 @@ unset MUX_KEY_FILE
237 # asserting against the developer's keyring. The legs that want an agent 237 # asserting against the developer's keyring. The legs that want an agent
238 # export one per command, at a path this file made. 238 # export one per command, at a path this file made.
239 unset SSH_AUTH_SOCK 239 unset SSH_AUTH_SOCK
240 # Second daemon, used only by the M7 abort scenario.
241 SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock"
242 defer_sock "$SOCK2"
243 # Third daemon, for the restart scenario: it gets killed and started again on
244 # the same path, so it cannot share the long-lived one.
245 SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock"
246 defer_sock "$SOCK3"
247 # Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP
248 # port, so it gets its own path rather than sharing the long-lived one.
249 SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock"
250 defer_sock "$SOCK4" "$SOCK4.nokey" "$SOCK4.second"
251 QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$"
252 defer_rm "$QKEY" "$QKEY.wrong" "$QKEY.bad"
253 # A port out of the way of the ephemeral range, made per-run so two suites can
254 # overlap. Collisions surface as a loud bind failure, never as a silent pass.
255 QPORT=$(( 21000 + ($$ % 4000) ))
256 # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it
257 # controls, so they cannot share the long-lived /bin/sh daemon.
258 SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock"
259 defer_sock "$SOCK5"
260 SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock"
261 defer_sock "$SOCK6"
262 SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock"
263 defer_sock "$SOCK7"
264 PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh"
265 defer_rm "$PWSH"
266 # M10 key-resolution daemons. Two, not one: the first scenario's client
267 # detaches and its daemon is killed, and the second must not be sharing
268 # either. Ports in bands of their own so a concurrent suite cannot collide.
269 SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock"
270 defer_sock "$SOCK9"
271 SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock"
272 defer_sock "$SOCK10"
273 QPORT2=$(( 26000 + ($$ % 4000) ))
274 QPORT3=$(( 31000 + ($$ % 4000) ))
275 # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never
276 # holds their pids as shell jobs — it reads them off the up-line and kills
277 # by that tracked pid, never by name.
278 SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"
279 defer_sock "$SOCK8"
280 SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock"
281 defer_sock "$SOCK8T"
282 SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock"
283 defer_sock "$SOCK11"
284 QPORT4=$(( 36000 + ($$ % 4000) ))
285 # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2
286 # resizes the grid twice and tp1's session is a scrollback-generating
287 # wrapper, so neither can share the long-lived /bin/sh daemon.
288 SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock"
289 defer_sock "$SOCK12"
290 SOCK13="${TMPDIR:-/tmp}/muxd-e2e-tp1-$$.sock"
291 defer_sock "$SOCK13"
292 # tp1's session: a wrapper that fills the scrollback and then becomes cat.
293 TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh"
294 defer_rm "$TP1SH"
295 # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the
296 # script is mid-flight — so its pid is tracked and killed like a daemon's.
297 # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY
298 # the proxy, the pty leg's by local mux under the M12 fixture. Both are
299 # torn down by `muxd stop` — the verb under test is also the cleanup, and
300 # the trap only backstops it. Neither daemon is a child of this shell, so
301 # each pid is read off the up-line its own spawner printed, which is the
302 # only handle that can never name a bystander.
303 SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock"
304 defer_sock "$SOCK14"
305 SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock"
306 defer_sock "$SOCK15"
307 # M14 ssh→QUIC handoff. The shim that stands in for ssh (built where the
308 # scenarios run), and a RUNTIME DIR
309 # per daemon: the handoff dials the DEFAULT socket on the far side, and the
310 # far side is this box, so each scenario's default socket has to be one of
311 # ours rather than the operator's. Two daemons, no more — the one a cold
312 # attach auto-starts, and the key-mismatch one that cannot be shared
313 # because it holds a key nothing else on this box has.
314 SSHIM_DIR="${TMPDIR:-/tmp}/muxd-e2e-sshim-$$"
315 defer_rm "$SSHIM_DIR"
316 SSHIM_PIDLOG="$SSHIM_DIR/pids"
317 export SSHIM_PIDLOG
318 HRUN="${TMPDIR:-/tmp}/mux-e2e-hrun-$$"
319 defer_rm "$HRUN"
320 HRUN2="${TMPDIR:-/tmp}/mux-e2e-hrun2-$$"
321 defer_rm "$HRUN2"
322 SOCK16="$HRUN/muxd.sock"
323 SOCK17="$HRUN2/muxd.sock"
324 defer_sock "$SOCK16" "$SOCK17"
325 HKEY="${TMPDIR:-/tmp}/mux-e2e-hkey-$$"
326 defer_rm "$HKEY"
327 # A config home that is a FILE: `muxd endpoint` cannot create a key under
328 # it and cannot find one, which is the announce-none scenario's lever.
329 HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$"
330 defer_rm "$HCFGBAD"
331 # Bands of their own, like every other port here, so a concurrent suite
332 # cannot collide — and BELOW the ephemeral range (32768–60999 here), which
333 # for HDEADPORT is not housekeeping but the scenario's correctness. It is
334 # the port the poisoned cache points at, and the daemon it is poisoning
335 # against holds a KERNEL-ASSIGNED ephemeral port: land on that one and the
336 # "dead" port is live, the dial succeeds, and the self-heal scenario
337 # quietly asserts a heal that never had to happen.
338 HQPORT=$(( 11000 + ($$ % 4000) ))
339 HDEADPORT=$(( 16000 + ($$ % 4000) ))
340 # The pipe_mux client in flight, and its FIFO. Both are registered by
341 # pipe_mux itself; its `timeout` stays the primary guarantee and the trap
342 # is the backstop for a scenario that exits between pipe_mux and
343 # pipe_detach, leaving a client the shell would otherwise orphan.
344 # M-web. Three daemons (the passivity pin's, the hub-basic one, and the
345 # tear scenario's two incarnations share SOCK20), two hub processes, and
346 # a port band of its own following the 5000-spacing convention — INSIDE
347 # the ephemeral range, which is fine for ports we bind (only provably-DEAD
348 # ports must stay below 32768; see HDEADPORT above).
349 SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock"
350 defer_sock "$SOCK18"
351 SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock"
352 defer_sock "$SOCK19"
353 SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock"
354 defer_sock "$SOCK20"
355 WPORT=$(( 41000 + ($$ % 4000) ))
356 WPORT2=$(( 46000 + ($$ % 4000) ))
357 # M18. Two daemons — the CLI multi-session block's and the wall's — and one
358 # more hub on the next 5000-spaced port. Only two sockets for four-and-some
359 # sessions, which is the whole point of the milestone: sessions are named on
360 # one socket, not spread across one socket each.
361 SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock"
362 defer_sock "$SOCK21"
363 SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock"
364 defer_sock "$SOCK22"
365 WPORT3=$(( 51000 + ($$ % 4000) ))
366 # ...and a third, the only M18 daemon holding a UDP port: it serves the
367 # session socket AND a QUIC listener at once, which is what lets one block
368 # ask whether a session name means the same session on either transport.
369 # Next 5000-spaced band after WPORT3.
370 SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock"
371 defer_sock "$SOCK23"
372 QPORT5=$(( 56000 + ($$ % 4000) ))
373 # A key of its own rather than the M8 block's $QKEY: that one is rm_swept
374 # the moment the M8 scenarios finish (see the sweep after the key-source
375 # block), which is hundreds of lines before this one runs. Reaching for a
376 # deleted file would make this block's daemon fail to bind for a reason
377 # that has nothing to do with sessions.
378 M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$"
379 defer_rm "$M18KEY"
380
381 # The bell leg's own daemon. A socket of its own is load-bearing rather than
382 # tidiness, and the argument is at the leg itself: that scenario counts BEL
383 # BYTES in a host capture, and a BEL is also what terminates an OSC. $SOCK's
384 # session has a window title set on it by the title leg, and every client
385 # attaching there is told so as `ESC]0;...BEL` — which would land in the
386 # capture and make the count assert something other than what it says.
387 SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock"
388 defer_sock "$SOCK24"
389
390 # The dynamic-wall leg: one daemon, one hub restarted three times, and a
391 # STATE HOME of its own on the 61000 band — the last 5000-spaced one that
392 # fits under 65535, the QUIC block's 56000 being the one before. The state
393 # home is the load-bearing part: this leg reads the wall file back as an
394 # artifact, and $XDG_STATE_HOME above is shared with every other scenario —
395 # the M-web and M18 hubs write their argv walls there, so a wall read out of
396 # it would be some other block's. Never the developer's ~/.local/state either; that is
397 # why every muxweb here is spawned with the override in front of it.
398 SOCK25="${TMPDIR:-/tmp}/muxd-e2e-dynwall-$$.sock"
399 defer_sock "$SOCK25"
400 WPORT4=$(( 61000 + ($$ % 4000) ))
401 DWSTATE="${TMPDIR:-/tmp}/mux-e2e-dynwall-state-$$"
402 defer_rm "$DWSTATE"
403
404 # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be
405 # confused with any other block's.
406 SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock"
407 defer_sock "$SOCK26"
408
409 # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord
410 # that fails to detach cannot strand another block's session.
411 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock"
412 defer_sock "$SOCK27"
413
414 # Ctrl-\ c (create a session and switch to it): its own daemon, because the
415 # assertion is about which sessions EXIST — another block's session on the
416 # same daemon would change the name the chord picks.
417 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock"
418 defer_sock "$SOCK28"
419
420 # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same
421 # reason as SOCK28, and more sharply — the ring IS the list of sessions on
422 # one daemon, so a stray session from another block would change where a
423 # step lands.
424 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock"
425 defer_sock "$SOCK29"
426
427 # M5 (the self-attach refusal): its own daemon because the assertion reads
428 # the SOCKET PATH out of a session shell's environment and compares it to
429 # the one this block started — a shared daemon would make the comparison a
430 # tautology about whichever block bound first.
431 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
432 defer_sock "$SOCK31"
433
434 # The wheel pair. Each needs a scrollback of its own to scroll (or to prove
435 # it did not), which the long-lived /bin/sh daemon has no way to hold still,
436 # and the second one needs a session that ASKS for the mouse — a mode that
437 # would follow every other scenario sharing the daemon around.
438 SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock"
439 defer_sock "$SOCK33"
440 WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh"
441 defer_rm "$WHEELSH"
442 SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock"
443 defer_sock "$SOCK34"
444 MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh"
445 defer_rm "$MOUSESH"
446 # The alternate-screen half of the wheel: its own daemon because the
447 # session under test is a PAGER holding the alt screen for its whole life,
448 # which no other scenario could share a grid with. And the no-terminal
449 # half, whose session is a bare `cat` so that what reaches the pty is
450 # echoed back into the grid the assertion reads.
451 SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock"
452 defer_sock "$SOCK35"
453 LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh"
454 defer_rm "$LESSSH"
455 LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt"
456 defer_rm "$LESSDATA"
457 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock"
458 defer_sock "$SOCK36"
459
460 # The focus-skip leg, on a daemon of its own for the grid-size reason: it
461 # asserts on grid sizes and on how many clients a session ever had at once,
462 # and a session another block created — or another block's client sitting
463 # on this daemon — would make both numbers say nothing.
464 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock"
465 defer_sock "$SOCK37"
466 # The dead tile. Its own daemon because the leg needs a session name that
467 # does NOT exist — the daemon refuses the attach — and any other block's
468 # daemon might have one by that name.
469 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock"
470 defer_sock "$SOCK39"
471
472 # The wheel inside the focused tile. Two daemons for the wheel pair's own
473 # reason, turned up one notch: each needs a session whose SHELL is fixed for
474 # the session's whole life — one that scrolls and one that ASKS for the
475 # mouse — and a mode another scenario's session set would follow the wall's
476 # terminal around. Both use the daemon's DEFAULT session, so the wall
477 # spelling is a bare `--sock PATH` and nothing has to create anything.
478 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock"
479 defer_sock "$SOCK41"
480 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh"
481 defer_rm "$ZWHEELSH"
482 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
483 defer_sock "$SOCK42"
484 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
485 defer_rm "$ZMOUSESH"
486
487 # The scrollback-rect leg: a focused tile's history page must own only its
488 # sub-rect, so its daemon gets a socket of its own for the grid-size
489 # reason every wall leg has.
490 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
491 defer_sock "$SOCK53"
492 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock"
493 defer_sock "$SOCK54"
494 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock"
495 defer_sock "$SOCK55"
496 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock"
497 defer_sock "$SOCK56"
498 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock"
499 defer_sock "$SOCK57"
500 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock"
501 defer_sock "$SOCK58"
502 SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock"
503 defer_sock "$SOCK59"
504 SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock"
505 defer_sock "$SOCK60"
506 SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock"
507 defer_sock "$SOCK61"
508 SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock"
509 defer_sock "$SOCK62"
510 SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock"
511 defer_sock "$SOCK63"
512 # The hydrated-create leg. One daemon, and it serves QUIC as well as its
513 # socket: the leg's whole point is the LOCAL/remote split, so both halves
514 # have to be the same run of the same daemon or the comparison is between
515 # two rigs rather than between two kinds of line. Short socket name on
516 # purpose — the assertion reads a label bar back off a 70-column render.
517 SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock"
518 defer_sock "$SOCK64"
519 # Base 6000, below the 5000-spaced band the other QUIC ports share: every
520 # base from 11000 up is already taken, and a DUPLICATED base passes only
521 # for as long as the other leg's daemon happens to be dead by the time
522 # this one binds. A collision reads as this daemon's failure, not the
523 # suite's, which is why the base has to be the leg's own.
524 HYPORT=$(( 6000 + ($$ % 4000) ))
525 HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$"
526 defer_rm "$HYKEY"
527 HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$"
528 defer_rm "$HYSTATE"
529 HYWALL="$HYSTATE/mux/wall"
530 # The refused-attach leg: its own daemon, because its assertions are about
531 # what a daemon says to a name it does NOT have, and a socket shared with
532 # another leg would let that leg's sessions decide what "does not have"
533 # means.
534 SOCK66="${TMPDIR:-/tmp}/muxd-e2e-refuse-$$.sock"
535 defer_sock "$SOCK66"
536 REFSTATE="${TMPDIR:-/tmp}/mux-e2e-refuse-state-$$"
537 defer_rm "$REFSTATE"
538 # The browser half of the same ruling: one daemon serving a socket AND a
539 # QUIC listener, one hub, a state home of its own. Two ports, and the
540 # 5000-spaced bands are exhausted — 61000 is the last one that fits under
541 # 65535 — so this leg takes the tail ABOVE the ephemeral range
542 # (ip_local_port_range tops out at 60999 by default), halved. The narrow
543 # `%250` is the price of that tail: two runs whose pids differ by 250
544 # collide where the older bands need 4000. A collision reads as this leg's
545 # daemon failing to bind, which is also why the two halves cannot share a
546 # base.
547 SOCK67="${TMPDIR:-/tmp}/muxd-e2e-wg-$$.sock"
548 defer_sock "$SOCK67"
549 WGPORT=$(( 65000 + ($$ % 250) ))
550 WGQPORT=$(( 65250 + ($$ % 250) ))
551 WGKEY="${TMPDIR:-/tmp}/mux-e2e-wgkey-$$"
552 defer_rm "$WGKEY"
553 WGSTATE="${TMPDIR:-/tmp}/mux-e2e-wg-state-$$"
554 defer_rm "$WGSTATE"
555 WGWALL="$WGSTATE/mux/wall"
556 # The refusal-spin leg. Its own everything: a shim dir on PATH, a state
557 # home holding the wall it restores, and a dial log the shim appends to.
558 SOCK68="${TMPDIR:-/tmp}/muxd-e2e-sp-$$.sock"
559 defer_sock "$SOCK68"
560 SPPORT=$(( 26000 + ($$ % 4000) ))
561 SPSTATE="${TMPDIR:-/tmp}/mux-e2e-sp-state-$$"
562 defer_rm "$SPSTATE"
563 SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$"
564 defer_rm "$SPDIR"
565 SPINLOG="$SPDIR/dials"
566 # `muxd upgrade` — four daemons, because each one is a different daemon
567 # LIFECYCLE and no two can share a process: the same-binary leg's daemon
568 # ends up running a second image, the rollback leg's is born with the abort
569 # armed in its environment, the agent leg's holds an `-A` client, and the
570 # QUIC leg's owns a UDP socket. They run one after another, so the last of
571 # them is the only leg here that needs a port.
572 SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock"
573 defer_sock "$SOCK69"
574 SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock"
575 defer_sock "$SOCK70"
576 SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock"
577 defer_sock "$SOCK71"
578 SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock"
579 defer_sock "$SOCK72"
580 SOCK73="${TMPDIR:-/tmp}/muxd-e2e-stopgone-$$.sock"
581 defer_sock "$SOCK73"
582 # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from
583 # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken
584 # for by the two browser ports. A collision here reads as this leg's daemon
585 # failing to bind, the same verdict every other band gives.
586 UPQPORT=$(( 2000 + ($$ % 4000) ))
587 UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$"
588 defer_rm "$UPKEY"
589 # A HOME of its own for the session shell. The title assertion below is
590 # about a title the SESSION set, and a distribution's rc file repaints the
591 # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says
592 # so of Arch's) — read out of the developer's own dotfiles, this leg would
593 # assert on whatever their prompt happens to spell.
594 UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$"
595 defer_rm "$UPHOME"
596 # The candidate binary the refusal leg offers: a copy, so its exec bit can
597 # be taken away without touching the one every other scenario runs.
598 UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$"
599 defer_rm "$UPBIN"
600 UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock"
601 defer_rm "$UPAGENT"
602 UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$"
603 defer_rm "$UPAGKEY" "$UPAGKEY.pub"
604
605 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
606 # own, for the dynamic-wall leg's reason turned up one notch: what these
607 # two blocks read back is the wall FILE, and every `mux` in this suite now
608 # writes a tile into the shared $XDG_STATE_HOME — so a file read out of
609 # that one would be every other block's attaches as much as this one's.
610 # The daemon is separate because the tile spelling contains the socket
611 # path, which is what the assertions grep for.
612 SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock"
613 defer_sock "$SOCK40"
614 WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$"
615 defer_rm "$WHSTATE"
616 WHWALL="$WHSTATE/mux/wall"
617 # A state home whose wall FILE is a directory: the unwritable case, which
618 # has to warn and let the attach happen anyway.
619 WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$"
620 defer_rm "$WHBAD"
621 # And a third, for the `x` block: its wall is built by two attaches and
622 # then eaten by `x`, so it must start empty and stay its own.
623 WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$"
624 defer_rm "$WHXSTATE"
625
626 # The convergence block: `mux TARGET` is a wall of one tile whose rect is
627 # the whole terminal. Three daemons and three state homes, each of them its own
628 # for the same reason the attach-history block's is — what these legs read
629 # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the
630 # suite has ever attached to.
631 SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock"
632 defer_sock "$SOCK45"
633 CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$"
634 defer_rm "$CVSTATE"
635 CVWALL="$CVSTATE/mux/wall"
636 # The ring-grows-the-wall leg needs TWO state homes: one for the client
637 # under test, and one for the setup client whose attach must NOT leave a
638 # tile the leg would then find already there. That absence is the case
639 # under test — a sibling the daemon knows and the wall does not.
640 SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock"
641 defer_sock "$SOCK46"
642 RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$"
643 defer_rm "$RGSTATE"
644 RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$"
645 defer_rm "$RGOTHER"
646 # The exit-semantics leg: a session ending in the focused tile.
647 SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock"
648 defer_sock "$SOCK47"
649 XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$"
650 defer_rm "$XESTATE"
651
652 # Agent forwarding: one socket path, two daemons in turn. The positive leg
653 # ends by typing `exit`, which ends that daemon's only session and takes the
654 # daemon with it — so the refusal leg below starts its own on the freed path
655 # rather than inheriting one that is already gone. Both are $MUXD; neither
656 # may be auto-started (see the $PATH note at the top of this file).
657 #
658 # The agent lives beside the sockets rather than under $OUT: a unix path is
659 # capped at 107 bytes by sun_path, and this one is dialled from inside a
660 # session shell, so the room it has is the room every other socket in this
661 # file was given.
662 SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock"
663 defer_sock "$SOCK48"
664 SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock"
665 defer_sock "$SOCK51"
666 SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock"
667 defer_sock "$SOCK52"
668 AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock"
669 defer_rm "$AGENT48"
670 AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$"
671 defer_rm "$AGENT48KEY" "$AGENT48KEY.pub"
672 D42PID=""
673
674 # The flip leg gets a daemon of its own, and not for isolation's sake: the
675 # one above is stopped by its own scenario's assert_stopped, so there is no
676 # daemon left to reuse. Two agents, two keys, two sockets — one agent behind
677 # both clients would print the same fingerprint whichever slot answered, and
678 # the leg would pass having proved nothing. Beside the sockets for the
679 # sun_path reason above; these are dialled from inside a session too.
680 SOCK49="${TMPDIR:-/tmp}/muxd-e2e-agentflip-$$.sock"
681 defer_sock "$SOCK49"
682 AGENT49A="${TMPDIR:-/tmp}/mux-e2e-agent2a-$$.sock"
683 defer_rm "$AGENT49A"
684 AGENT49B="${TMPDIR:-/tmp}/mux-e2e-agent2b-$$.sock"
685 defer_rm "$AGENT49B"
686 AGENT49APID=""
687 AGENT49BPID=""
688 AGENT49AKEY="${TMPDIR:-/tmp}/mux-e2e-agent2akey-$$"
689 defer_rm "$AGENT49AKEY" "$AGENT49AKEY.pub"
690 AGENT49BKEY="${TMPDIR:-/tmp}/mux-e2e-agent2bkey-$$"
691 defer_rm "$AGENT49BKEY" "$AGENT49BKEY.pub"
692 D43PID=""
693 FLIPAPID=""
694
695 # The wall's own mouse claim needs a daemon of its own for the same reason
696 # every other leg does: the wall scenario above stops its daemon on the way
697 # out, so there is nothing left to reuse.
698 SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock"
699 defer_sock "$SOCK50"
700 D48PID=""
701 240
702 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 241 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
703 # such line on exit; every field is a key=value pair, so a rename or reorder 242 # such line on exit; every field is a key=value pair, so a rename or reorder
@@ -1616,6 +1155,42 @@ cleanup() {
1616 } 1155 }
1617 trap cleanup EXIT INT TERM 1156 trap cleanup EXIT INT TERM
1618 1157
1158 # Second daemon, used only by the M7 abort scenario.
1159 SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock"
1160 defer_sock "$SOCK2"
1161 # Third daemon, for the restart scenario: it gets killed and started again on
1162 # the same path, so it cannot share the long-lived one.
1163 SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock"
1164 defer_sock "$SOCK3"
1165 # Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP
1166 # port, so it gets its own path rather than sharing the long-lived one.
1167 SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock"
1168 defer_sock "$SOCK4" "$SOCK4.nokey" "$SOCK4.second"
1169 QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$"
1170 defer_rm "$QKEY" "$QKEY.wrong" "$QKEY.bad"
1171 # A port out of the way of the ephemeral range, made per-run so two suites can
1172 # overlap. Collisions surface as a loud bind failure, never as a silent pass.
1173 QPORT=$(( 21000 + ($$ % 4000) ))
1174 # M10 key-resolution daemons. Two, not one: the first scenario's client
1175 # detaches and its daemon is killed, and the second must not be sharing
1176 # either. Ports in bands of their own so a concurrent suite cannot collide.
1177 SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock"
1178 defer_sock "$SOCK9"
1179 SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock"
1180 defer_sock "$SOCK10"
1181 QPORT2=$(( 26000 + ($$ % 4000) ))
1182 QPORT3=$(( 31000 + ($$ % 4000) ))
1183 # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never
1184 # holds their pids as shell jobs — it reads them off the up-line and kills
1185 # by that tracked pid, never by name.
1186 SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock"
1187 defer_sock "$SOCK8"
1188 SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock"
1189 defer_sock "$SOCK8T"
1190 SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock"
1191 defer_sock "$SOCK11"
1192 QPORT4=$(( 36000 + ($$ % 4000) ))
1193
1619 # --- M10: --version answers "did the scp land" without a daemon anywhere. 1194 # --- M10: --version answers "did the scp land" without a daemon anywhere.
1620 # No convergence: no daemon and no client, so there is no grid on either side. 1195 # No convergence: no daemon and no client, so there is no grid on either side.
1621 "$MUXD" --version | grep -q '^muxd 0\.' || { echo "e2e FAIL: muxd --version"; exit 1; } 1196 "$MUXD" --version | grep -q '^muxd 0\.' || { echo "e2e FAIL: muxd --version"; exit 1; }
@@ -2542,6 +2117,27 @@ softkill "$GPID" || true
2542 GPID="" 2117 GPID=""
2543 ok "keygen + start --quic + mux quic:// with no --key anywhere" 2118 ok "keygen + start --quic + mux quic:// with no --key anywhere"
2544 2119
2120 # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it
2121 # controls, so they cannot share the long-lived /bin/sh daemon.
2122 SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock"
2123 defer_sock "$SOCK5"
2124 SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock"
2125 defer_sock "$SOCK6"
2126 SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock"
2127 defer_sock "$SOCK7"
2128 PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh"
2129 defer_rm "$PWSH"
2130 # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2
2131 # resizes the grid twice and tp1's session is a scrollback-generating
2132 # wrapper, so neither can share the long-lived /bin/sh daemon.
2133 SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock"
2134 defer_sock "$SOCK12"
2135 SOCK13="${TMPDIR:-/tmp}/muxd-e2e-tp1-$$.sock"
2136 defer_sock "$SOCK13"
2137 # tp1's session: a wrapper that fills the scrollback and then becomes cat.
2138 TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh"
2139 defer_rm "$TP1SH"
2140
2545 # ---- M9: prediction --------------------------------------------------- 2141 # ---- M9: prediction ---------------------------------------------------
2546 # 2142 #
2547 # Every scenario here asserts COUNTERS, because the screen cannot tell a 2143 # Every scenario here asserts COUNTERS, because the screen cannot tell a
@@ -3321,6 +2917,27 @@ D13PID=""
3321 rm_swept "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" "$OUT.tp1.d" "$TP1SH" 2917 rm_swept "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" "$OUT.tp1.d" "$TP1SH"
3322 ok "reconnect while scrolled: view restored, prediction resumed" 2918 ok "reconnect while scrolled: view restored, prediction resumed"
3323 2919
2920 # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the
2921 # script is mid-flight — so its pid is tracked and killed like a daemon's.
2922 # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY
2923 # the proxy, the pty leg's by local mux under the M12 fixture. Both are
2924 # torn down by `muxd stop` — the verb under test is also the cleanup, and
2925 # the trap only backstops it. Neither daemon is a child of this shell, so
2926 # each pid is read off the up-line its own spawner printed, which is the
2927 # only handle that can never name a bystander.
2928 SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock"
2929 defer_sock "$SOCK14"
2930 SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock"
2931 defer_sock "$SOCK15"
2932 # The bell leg's own daemon. A socket of its own is load-bearing rather than
2933 # tidiness, and the argument is at the leg itself: that scenario counts BEL
2934 # BYTES in a host capture, and a BEL is also what terminates an OSC. $SOCK's
2935 # session has a window title set on it by the title leg, and every client
2936 # attaching there is told so as `ESC]0;...BEL` — which would land in the
2937 # capture and make the count assert something other than what it says.
2938 SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock"
2939 defer_sock "$SOCK24"
2940
3324 # --- M13: attach auto-start (proxy) + muxd stop ------------------------ 2941 # --- M13: attach auto-start (proxy) + muxd stop ------------------------
3325 # 2942 #
3326 # Nothing is serving SOCK14: the attach itself must produce the daemon. 2943 # Nothing is serving SOCK14: the attach itself must produce the daemon.
@@ -3854,6 +3471,53 @@ D21PID=""
3854 rm_swept "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" "$OUT.d21.d" 3471 rm_swept "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" "$OUT.d21.d"
3855 ok "a burst of bells in one chunk reaches the host as exactly one" 3472 ok "a burst of bells in one chunk reaches the host as exactly one"
3856 3473
3474 # The long-lived daemon has served every scenario that wanted it; stop it
3475 # NOW so its allocator verdict is written while the suite is still running
3476 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
3477 # (and the verdict) runs.
3478 #
3479 # The sweep that READS that verdict is in the EXIT trap, not here: this line
3480 # is only reached by a run that passed, and a leak deserves reporting on the
3481 # runs that did not (see leak_sweep). Everything below it is a pin, and the
3482 # trap fires after all of them either way.
3483 softkill "$D1PID" || true
3484 wait "$D1PID" 2>/dev/null || true
3485 D1PID=""
3486
3487 # M14 ssh→QUIC handoff. The shim that stands in for ssh (built where the
3488 # scenarios run), and a RUNTIME DIR
3489 # per daemon: the handoff dials the DEFAULT socket on the far side, and the
3490 # far side is this box, so each scenario's default socket has to be one of
3491 # ours rather than the operator's. Two daemons, no more — the one a cold
3492 # attach auto-starts, and the key-mismatch one that cannot be shared
3493 # because it holds a key nothing else on this box has.
3494 SSHIM_DIR="${TMPDIR:-/tmp}/muxd-e2e-sshim-$$"
3495 defer_rm "$SSHIM_DIR"
3496 SSHIM_PIDLOG="$SSHIM_DIR/pids"
3497 export SSHIM_PIDLOG
3498 HRUN="${TMPDIR:-/tmp}/mux-e2e-hrun-$$"
3499 defer_rm "$HRUN"
3500 HRUN2="${TMPDIR:-/tmp}/mux-e2e-hrun2-$$"
3501 defer_rm "$HRUN2"
3502 SOCK16="$HRUN/muxd.sock"
3503 SOCK17="$HRUN2/muxd.sock"
3504 defer_sock "$SOCK16" "$SOCK17"
3505 HKEY="${TMPDIR:-/tmp}/mux-e2e-hkey-$$"
3506 defer_rm "$HKEY"
3507 # A config home that is a FILE: `muxd endpoint` cannot create a key under
3508 # it and cannot find one, which is the announce-none scenario's lever.
3509 HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$"
3510 defer_rm "$HCFGBAD"
3511 # Bands of their own, like every other port here, so a concurrent suite
3512 # cannot collide — and BELOW the ephemeral range (32768–60999 here), which
3513 # for HDEADPORT is not housekeeping but the scenario's correctness. It is
3514 # the port the poisoned cache points at, and the daemon it is poisoning
3515 # against holds a KERNEL-ASSIGNED ephemeral port: land on that one and the
3516 # "dead" port is live, the dial succeeds, and the self-heal scenario
3517 # quietly asserts a heal that never had to happen.
3518 HQPORT=$(( 11000 + ($$ % 4000) ))
3519 HDEADPORT=$(( 16000 + ($$ % 4000) ))
3520
3857 # --- M14: the ssh→QUIC handoff ----------------------------------------- 3521 # --- M14: the ssh→QUIC handoff -----------------------------------------
3858 # 3522 #
3859 # `mux HOST` fetches QUIC coordinates over ssh once, caches them, and 3523 # `mux HOST` fetches QUIC coordinates over ssh once, caches them, and
@@ -4259,6 +3923,30 @@ rm_swept "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err"
4259 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD" 3923 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD"
4260 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" 3924 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2"
4261 3925
3926 # M18. Two daemons — the CLI multi-session block's and the wall's — and one
3927 # more hub on the next 5000-spaced port. Only two sockets for four-and-some
3928 # sessions, which is the whole point of the milestone: sessions are named on
3929 # one socket, not spread across one socket each.
3930 SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock"
3931 defer_sock "$SOCK21"
3932 SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock"
3933 defer_sock "$SOCK22"
3934 WPORT3=$(( 51000 + ($$ % 4000) ))
3935 # ...and a third, the only M18 daemon holding a UDP port: it serves the
3936 # session socket AND a QUIC listener at once, which is what lets one block
3937 # ask whether a session name means the same session on either transport.
3938 # Next 5000-spaced band after WPORT3.
3939 SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock"
3940 defer_sock "$SOCK23"
3941 QPORT5=$(( 56000 + ($$ % 4000) ))
3942 # A key of its own rather than the M8 block's $QKEY: that one is rm_swept
3943 # the moment the M8 scenarios finish (see the sweep after the key-source
3944 # block), which is hundreds of lines before this one runs. Reaching for a
3945 # deleted file would make this block's daemon fail to bind for a reason
3946 # that has nothing to do with sessions.
3947 M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$"
3948 defer_rm "$M18KEY"
3949
4262 # --- M18: two sessions on one daemon are two shells --------------------- 3950 # --- M18: two sessions on one daemon are two shells ---------------------
4263 # The milestone, observed from outside: ONE daemon, one socket path, one 3951 # The milestone, observed from outside: ONE daemon, one socket path, one
4264 # key — and two sessions that share nothing. A connection IS a session 3952 # key — and two sessions that share nothing. A connection IS a session
@@ -4569,6 +4257,37 @@ assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop"
4569 D20PID="" 4257 D20PID=""
4570 ok "quic: two dials are two sessions, and one name is one session on either transport" 4258 ok "quic: two dials are two sessions, and one name is one session on either transport"
4571 4259
4260 # The pipe_mux client in flight, and its FIFO. Both are registered by
4261 # pipe_mux itself; its `timeout` stays the primary guarantee and the trap
4262 # is the backstop for a scenario that exits between pipe_mux and
4263 # pipe_detach, leaving a client the shell would otherwise orphan.
4264 # M-web. Three daemons (the passivity pin's, the hub-basic one, and the
4265 # tear scenario's two incarnations share SOCK20), two hub processes, and
4266 # a port band of its own following the 5000-spacing convention — INSIDE
4267 # the ephemeral range, which is fine for ports we bind (only provably-DEAD
4268 # ports must stay below 32768; see HDEADPORT above).
4269 SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock"
4270 defer_sock "$SOCK18"
4271 SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock"
4272 defer_sock "$SOCK19"
4273 SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock"
4274 defer_sock "$SOCK20"
4275 WPORT=$(( 41000 + ($$ % 4000) ))
4276 WPORT2=$(( 46000 + ($$ % 4000) ))
4277 # The dynamic-wall leg: one daemon, one hub restarted three times, and a
4278 # STATE HOME of its own on the 61000 band — the last 5000-spaced one that
4279 # fits under 65535, the QUIC block's 56000 being the one before. The state
4280 # home is the load-bearing part: this leg reads the wall file back as an
4281 # artifact, and $XDG_STATE_HOME above is shared with every other scenario —
4282 # the M-web and M18 hubs write their argv walls there, so a wall read out of
4283 # it would be some other block's. Never the developer's ~/.local/state either; that is
4284 # why every muxweb here is spawned with the override in front of it.
4285 SOCK25="${TMPDIR:-/tmp}/muxd-e2e-dynwall-$$.sock"
4286 defer_sock "$SOCK25"
4287 WPORT4=$(( 61000 + ($$ % 4000) ))
4288 DWSTATE="${TMPDIR:-/tmp}/mux-e2e-dynwall-state-$$"
4289 defer_rm "$DWSTATE"
4290
4572 # --- a 1x1 attach is refused the grid and can never claim it. 4291 # --- a 1x1 attach is refused the grid and can never claim it.
4573 # The degenerate size a client can genuinely arrive with — a terminal one 4292 # The degenerate size a client can genuinely arrive with — a terminal one
4574 # column wide — driven here on a real 1x1 pty. Nothing spells passivity 4293 # column wide — driven here on a real 1x1 pty. Nothing spells passivity
@@ -4962,6 +4681,32 @@ assert_stopped "$SOCK25" "$D22PID" "dyn wall" "$OUT.dwstop"
4962 D22PID="" 4681 D22PID=""
4963 ok "the wall is runtime state: add, remove, reorder, restore, argv adds" 4682 ok "the wall is runtime state: add, remove, reorder, restore, argv adds"
4964 4683
4684 # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be
4685 # confused with any other block's.
4686 SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock"
4687 defer_sock "$SOCK26"
4688 # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord
4689 # that fails to detach cannot strand another block's session.
4690 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock"
4691 defer_sock "$SOCK27"
4692 # Ctrl-\ c (create a session and switch to it): its own daemon, because the
4693 # assertion is about which sessions EXIST — another block's session on the
4694 # same daemon would change the name the chord picks.
4695 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock"
4696 defer_sock "$SOCK28"
4697 # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same
4698 # reason as SOCK28, and more sharply — the ring IS the list of sessions on
4699 # one daemon, so a stray session from another block would change where a
4700 # step lands.
4701 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock"
4702 defer_sock "$SOCK29"
4703 # M5 (the self-attach refusal): its own daemon because the assertion reads
4704 # the SOCKET PATH out of a session shell's environment and compares it to
4705 # the one this block started — a shared daemon would make the comparison a
4706 # tautology about whichever block bound first.
4707 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
4708 defer_sock "$SOCK31"
4709
4965 # ---- the CLI wall: the hub's multiattach, in one terminal -------------- 4710 # ---- the CLI wall: the hub's multiattach, in one terminal --------------
4966 # One daemon, two named sessions, one `mux wall` on a real pty showing 4711 # One daemon, two named sessions, one `mux wall` on a real pty showing
4967 # both AT ONCE — the browser wall's claim, made by the CLI. Three facts, 4712 # both AT ONCE — the browser wall's claim, made by the CLI. Three facts,
@@ -5387,6 +5132,57 @@ assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop"
5387 D28PID="" 5132 D28PID=""
5388 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" 5133 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself"
5389 5134
5135 # The wheel pair. Each needs a scrollback of its own to scroll (or to prove
5136 # it did not), which the long-lived /bin/sh daemon has no way to hold still,
5137 # and the second one needs a session that ASKS for the mouse — a mode that
5138 # would follow every other scenario sharing the daemon around.
5139 SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock"
5140 defer_sock "$SOCK33"
5141 WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh"
5142 defer_rm "$WHEELSH"
5143 SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock"
5144 defer_sock "$SOCK34"
5145 MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh"
5146 defer_rm "$MOUSESH"
5147 # The alternate-screen half of the wheel: its own daemon because the
5148 # session under test is a PAGER holding the alt screen for its whole life,
5149 # which no other scenario could share a grid with. And the no-terminal
5150 # half, whose session is a bare `cat` so that what reaches the pty is
5151 # echoed back into the grid the assertion reads.
5152 SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock"
5153 defer_sock "$SOCK35"
5154 LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh"
5155 defer_rm "$LESSSH"
5156 LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt"
5157 defer_rm "$LESSDATA"
5158 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock"
5159 defer_sock "$SOCK36"
5160 # The focus-skip leg, on a daemon of its own for the grid-size reason: it
5161 # asserts on grid sizes and on how many clients a session ever had at once,
5162 # and a session another block created — or another block's client sitting
5163 # on this daemon — would make both numbers say nothing.
5164 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock"
5165 defer_sock "$SOCK37"
5166 # The dead tile. Its own daemon because the leg needs a session name that
5167 # does NOT exist — the daemon refuses the attach — and any other block's
5168 # daemon might have one by that name.
5169 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock"
5170 defer_sock "$SOCK39"
5171 # The wheel inside the focused tile. Two daemons for the wheel pair's own
5172 # reason, turned up one notch: each needs a session whose SHELL is fixed for
5173 # the session's whole life — one that scrolls and one that ASKS for the
5174 # mouse — and a mode another scenario's session set would follow the wall's
5175 # terminal around. Both use the daemon's DEFAULT session, so the wall
5176 # spelling is a bare `--sock PATH` and nothing has to create anything.
5177 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock"
5178 defer_sock "$SOCK41"
5179 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh"
5180 defer_rm "$ZWHEELSH"
5181 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
5182 defer_sock "$SOCK42"
5183 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
5184 defer_rm "$ZMOUSESH"
5185
5390 # ---- the wheel scrolls back, and an app that asks gets it instead ------- 5186 # ---- the wheel scrolls back, and an app that asks gets it instead -------
5391 # 5187 #
5392 # The pair is one claim seen from both sides, so the two legs are the same 5188 # The pair is one claim seen from both sides, so the two legs are the same
@@ -6000,6 +5796,52 @@ assert_stopped "$SOCK39" "$D36PID" "dead tile" "$OUT.zdstop"
6000 D36PID="" 5796 D36PID=""
6001 ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one" 5797 ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one"
6002 5798
5799 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
5800 # own, for the dynamic-wall leg's reason turned up one notch: what these
5801 # two blocks read back is the wall FILE, and every `mux` in this suite now
5802 # writes a tile into the shared $XDG_STATE_HOME — so a file read out of
5803 # that one would be every other block's attaches as much as this one's.
5804 # The daemon is separate because the tile spelling contains the socket
5805 # path, which is what the assertions grep for.
5806 SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock"
5807 defer_sock "$SOCK40"
5808 WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$"
5809 defer_rm "$WHSTATE"
5810 WHWALL="$WHSTATE/mux/wall"
5811 # A state home whose wall FILE is a directory: the unwritable case, which
5812 # has to warn and let the attach happen anyway.
5813 WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$"
5814 defer_rm "$WHBAD"
5815 # And a third, for the `x` block: its wall is built by two attaches and
5816 # then eaten by `x`, so it must start empty and stay its own.
5817 WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$"
5818 defer_rm "$WHXSTATE"
5819 # The convergence block: `mux TARGET` is a wall of one tile whose rect is
5820 # the whole terminal. Three daemons and three state homes, each of them its own
5821 # for the same reason the attach-history block's is — what these legs read
5822 # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the
5823 # suite has ever attached to.
5824 SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock"
5825 defer_sock "$SOCK45"
5826 CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$"
5827 defer_rm "$CVSTATE"
5828 CVWALL="$CVSTATE/mux/wall"
5829 # The ring-grows-the-wall leg needs TWO state homes: one for the client
5830 # under test, and one for the setup client whose attach must NOT leave a
5831 # tile the leg would then find already there. That absence is the case
5832 # under test — a sibling the daemon knows and the wall does not.
5833 SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock"
5834 defer_sock "$SOCK46"
5835 RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$"
5836 defer_rm "$RGSTATE"
5837 RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$"
5838 defer_rm "$RGOTHER"
5839 # The exit-semantics leg: a session ending in the focused tile.
5840 SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock"
5841 defer_sock "$SOCK47"
5842 XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$"
5843 defer_rm "$XESTATE"
5844
6003 # ---- the wall is attach HISTORY ----------------------------------------- 5845 # ---- the wall is attach HISTORY -----------------------------------------
6004 # 5846 #
6005 # The mechanical rule (wall-home-screen spec, phase 2): an attach that 5847 # The mechanical rule (wall-home-screen spec, phase 2): an attach that
@@ -6587,6 +6429,39 @@ assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop"
6587 D47PID="" 6429 D47PID=""
6588 ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour" 6430 ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour"
6589 6431
6432 # Agent forwarding: one socket path, two daemons in turn. The positive leg
6433 # ends by typing `exit`, which ends that daemon's only session and takes the
6434 # daemon with it — so the refusal leg below starts its own on the freed path
6435 # rather than inheriting one that is already gone. Both are $MUXD; neither
6436 # may be auto-started (see the $PATH note at the top of this file).
6437 #
6438 # The agent lives beside the sockets rather than under $OUT: a unix path is
6439 # capped at 107 bytes by sun_path, and this one is dialled from inside a
6440 # session shell, so the room it has is the room every other socket in this
6441 # file was given.
6442 SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock"
6443 defer_sock "$SOCK48"
6444 AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock"
6445 defer_rm "$AGENT48"
6446 AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$"
6447 defer_rm "$AGENT48KEY" "$AGENT48KEY.pub"
6448 # The flip leg gets a daemon of its own, and not for isolation's sake: the
6449 # one above is stopped by its own scenario's assert_stopped, so there is no
6450 # daemon left to reuse. Two agents, two keys, two sockets — one agent behind
6451 # both clients would print the same fingerprint whichever slot answered, and
6452 # the leg would pass having proved nothing. Beside the sockets for the
6453 # sun_path reason above; these are dialled from inside a session too.
6454 SOCK49="${TMPDIR:-/tmp}/muxd-e2e-agentflip-$$.sock"
6455 defer_sock "$SOCK49"
6456 AGENT49A="${TMPDIR:-/tmp}/mux-e2e-agent2a-$$.sock"
6457 defer_rm "$AGENT49A"
6458 AGENT49B="${TMPDIR:-/tmp}/mux-e2e-agent2b-$$.sock"
6459 defer_rm "$AGENT49B"
6460 AGENT49AKEY="${TMPDIR:-/tmp}/mux-e2e-agent2akey-$$"
6461 defer_rm "$AGENT49AKEY" "$AGENT49AKEY.pub"
6462 AGENT49BKEY="${TMPDIR:-/tmp}/mux-e2e-agent2bkey-$$"
6463 defer_rm "$AGENT49BKEY" "$AGENT49BKEY.pub"
6464
6590 # --- agent forwarding: a real key in the client's agent answers in the session 6465 # --- agent forwarding: a real key in the client's agent answers in the session
6591 # 6466 #
6592 # Everything under this heading is a real program doing its real job: a real 6467 # Everything under this heading is a real program doing its real job: a real
@@ -7012,6 +6887,12 @@ softkill "$AGENT49BPID" || true
7012 AGENT49BPID="" 6887 AGENT49BPID=""
7013 ok "agent forwarding: the agent that answers is whoever typed last" 6888 ok "agent forwarding: the agent that answers is whoever typed last"
7014 6889
6890 # The wall's own mouse claim needs a daemon of its own for the same reason
6891 # every other leg does: the wall scenario above stops its daemon on the way
6892 # out, so there is nothing left to reuse.
6893 SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock"
6894 defer_sock "$SOCK50"
6895
7015 # ---- the SOCK50 cluster: a daemon, two named sessions ------------------ 6896 # ---- the SOCK50 cluster: a daemon, two named sessions ------------------
7016 # 6897 #
7017 # A daemon and two named sessions (a, b) shared by the click leg below and 6898 # A daemon and two named sessions (a, b) shared by the click leg below and
@@ -7240,6 +7121,11 @@ assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop"
7240 D48PID="" 7121 D48PID=""
7241 ok "a drag copies on release, and a click copies nothing" 7122 ok "a drag copies on release, and a click copies nothing"
7242 7123
7124 # The in-band resize leg: a daemon of its own, so the grid it reports on is
7125 # the one this leg sized.
7126 SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock"
7127 defer_sock "$SOCK51"
7128
7243 # --- a resize is told in-band to an app that asked for it (mode 2048) ------ 7129 # --- a resize is told in-band to an app that asked for it (mode 2048) ------
7244 # 7130 #
7245 # nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and 7131 # nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and
@@ -7386,7 +7272,38 @@ wait_pid_gone "$D42PID" "agent-mute: the session ended and the daemon should fol
7386 D42PID="" 7272 D42PID=""
7387 ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)" 7273 ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)"
7388 7274
7275 # The scrollback-rect leg: a focused tile's history page must own only its
7276 # sub-rect, so its daemon gets a socket of its own for the grid-size
7277 # reason every wall leg has.
7278 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
7279 defer_sock "$SOCK53"
7280 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock"
7281 defer_sock "$SOCK54"
7282 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock"
7283 defer_sock "$SOCK55"
7284 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock"
7285 defer_sock "$SOCK56"
7286 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock"
7287 defer_sock "$SOCK57"
7288 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock"
7289 defer_sock "$SOCK58"
7290 SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock"
7291 defer_sock "$SOCK59"
7292 SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock"
7293 defer_sock "$SOCK60"
7294 SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock"
7295 defer_sock "$SOCK61"
7296 SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock"
7297 defer_sock "$SOCK62"
7298 SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock"
7299 defer_sock "$SOCK63"
7300
7389 # --- the cursor rests in the focused tile, whoever painted last ---------- 7301 # --- the cursor rests in the focused tile, whoever painted last ----------
7302 # The cursor-ownership leg: a daemon of its own, for the grid-size reason
7303 # every wall leg has — the assertion is about where the cursor RESTS, and a
7304 # session another block created would change the tile it rests in.
7305 SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock"
7306 defer_sock "$SOCK52"
7390 # 7307 #
7391 # Every painter ends by showing the cursor where its own stripe sits, so 7308 # Every painter ends by showing the cursor where its own stripe sits, so
7392 # without an owner the visible cursor lands on whichever pump painted last: 7309 # without an owner the visible cursor lands on whichever pump painted last:
@@ -7525,41 +7442,6 @@ assert_stopped "$SOCK53" "$D54PID" "scrollback-rect" "$OUT.sb2stop"
7525 D54PID="" 7442 D54PID=""
7526 ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's pin stays" 7443 ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's pin stays"
7527 7444
7528 # The long-lived daemon has served every scenario that wanted it; stop it
7529 # NOW so its allocator verdict is written while the suite is still running
7530 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
7531 # (and the verdict) runs.
7532 #
7533 # The sweep that READS that verdict is in the EXIT trap, not here: this line
7534 # is only reached by a run that passed, and a leak deserves reporting on the
7535 # runs that did not (see leak_sweep). Everything below it is a pin, and the
7536 # trap fires after all of them either way.
7537 softkill "$D1PID" || true
7538 wait "$D1PID" 2>/dev/null || true
7539 D1PID=""
7540
7541 # The pins. Literals, not variables set from counting something else —
7542 # "assert the literal, never the constant the code under test reads"
7543 # (decisions.md, M10). The two figures live at the END of this file and
7544 # nowhere else: a count repeated in prose is a count nothing gates, and
7545 # both of the ones that used to stand here had drifted away from the pins
7546 # they described. Adding a scenario means editing those two literals by
7547 # hand, on purpose.
7548 #
7549 # Why most legs carry no convergence point. `assert_converged` replays one
7550 # client capture into an engine and diffs it against one daemon grid, so it
7551 # can only speak where BOTH exist and the grid is the subject. It says
7552 # nothing about a side channel (OSC 52, a window title, pty modes, a
7553 # fingerprint ssh printed, bytes counted rather than read), about a file an
7554 # editor or the wall wrote, about an exit code or a status field, about
7555 # WHICH of two sessions a keystroke reached, about scrollback the live grid
7556 # does not hold, about the absence of bytes two agreeing empties would
7557 # agree on anyway, or about a leg that never attaches at all. It also does
7558 # not claim to reproduce a full-screen application's redraws byte for byte,
7559 # which is why the alternate-screen legs stay out. A leg that asserts on
7560 # one of those and adds a convergence point would be pinning a fact every
7561 # leg above already establishes.
7562
7563 # ---- fullscreen: f toggles, focus follows, restore ------------------- 7445 # ---- fullscreen: f toggles, focus follows, restore -------------------
7564 # 7446 #
7565 # `f` is a layout op, not a mode: the focused tile takes the whole 7447 # `f` is a layout op, not a mode: the focused tile takes the whole
@@ -8191,6 +8073,61 @@ D62PID=""
8191 rm -rf "$LPDSTATE" 8073 rm -rf "$LPDSTATE"
8192 ok "a corrupted sidecar degrades silently to the default layout" 8074 ok "a corrupted sidecar degrades silently to the default layout"
8193 8075
8076 # The hydrated-create leg. One daemon, and it serves QUIC as well as its
8077 # socket: the leg's whole point is the LOCAL/remote split, so both halves
8078 # have to be the same run of the same daemon or the comparison is between
8079 # two rigs rather than between two kinds of line. Short socket name on
8080 # purpose — the assertion reads a label bar back off a 70-column render.
8081 SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock"
8082 defer_sock "$SOCK64"
8083 # Base 6000, below the 5000-spaced band the other QUIC ports share: every
8084 # base from 11000 up is already taken, and a DUPLICATED base passes only
8085 # for as long as the other leg's daemon happens to be dead by the time
8086 # this one binds. A collision reads as this daemon's failure, not the
8087 # suite's, which is why the base has to be the leg's own.
8088 HYPORT=$(( 6000 + ($$ % 4000) ))
8089 HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$"
8090 defer_rm "$HYKEY"
8091 HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$"
8092 defer_rm "$HYSTATE"
8093 HYWALL="$HYSTATE/mux/wall"
8094 # The refused-attach leg: its own daemon, because its assertions are about
8095 # what a daemon says to a name it does NOT have, and a socket shared with
8096 # another leg would let that leg's sessions decide what "does not have"
8097 # means.
8098 SOCK66="${TMPDIR:-/tmp}/muxd-e2e-refuse-$$.sock"
8099 defer_sock "$SOCK66"
8100 REFSTATE="${TMPDIR:-/tmp}/mux-e2e-refuse-state-$$"
8101 defer_rm "$REFSTATE"
8102 # The browser half of the same ruling: one daemon serving a socket AND a
8103 # QUIC listener, one hub, a state home of its own. Two ports, and the
8104 # 5000-spaced bands are exhausted — 61000 is the last one that fits under
8105 # 65535 — so this leg takes the tail ABOVE the ephemeral range
8106 # (ip_local_port_range tops out at 60999 by default), halved. The narrow
8107 # `%250` is the price of that tail: two runs whose pids differ by 250
8108 # collide where the older bands need 4000. A collision reads as this leg's
8109 # daemon failing to bind, which is also why the two halves cannot share a
8110 # base.
8111 SOCK67="${TMPDIR:-/tmp}/muxd-e2e-wg-$$.sock"
8112 defer_sock "$SOCK67"
8113 WGPORT=$(( 65000 + ($$ % 250) ))
8114 WGQPORT=$(( 65250 + ($$ % 250) ))
8115 WGKEY="${TMPDIR:-/tmp}/mux-e2e-wgkey-$$"
8116 defer_rm "$WGKEY"
8117 WGSTATE="${TMPDIR:-/tmp}/mux-e2e-wg-state-$$"
8118 defer_rm "$WGSTATE"
8119 WGWALL="$WGSTATE/mux/wall"
8120 # The refusal-spin leg. Its own everything: a shim dir on PATH, a state
8121 # home holding the wall it restores, and a dial log the shim appends to.
8122 SOCK68="${TMPDIR:-/tmp}/muxd-e2e-sp-$$.sock"
8123 defer_sock "$SOCK68"
8124 SPPORT=$(( 26000 + ($$ % 4000) ))
8125 SPSTATE="${TMPDIR:-/tmp}/mux-e2e-sp-state-$$"
8126 defer_rm "$SPSTATE"
8127 SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$"
8128 defer_rm "$SPDIR"
8129 SPINLOG="$SPDIR/dials"
8130
8194 # ---- Ctrl-\ : adds a tile by spelling --------------------------------- 8131 # ---- Ctrl-\ : adds a tile by spelling ---------------------------------
8195 # 8132 #
8196 # Argv typed from inside. A spelling naming a session on ANOTHER daemon 8133 # Argv typed from inside. A spelling naming a session on ANOTHER daemon
@@ -8843,6 +8780,44 @@ D68PID=""
8843 rm -rf "$SPSTATE" "$SPDIR" 8780 rm -rf "$SPSTATE" "$SPDIR"
8844 ok "a refusal the birth cannot fix backs off instead of spinning" 8781 ok "a refusal the birth cannot fix backs off instead of spinning"
8845 8782
8783 # `muxd upgrade` — four daemons, because each one is a different daemon
8784 # LIFECYCLE and no two can share a process: the same-binary leg's daemon
8785 # ends up running a second image, the rollback leg's is born with the abort
8786 # armed in its environment, the agent leg's holds an `-A` client, and the
8787 # QUIC leg's owns a UDP socket. They run one after another, so the last of
8788 # them is the only leg here that needs a port.
8789 SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock"
8790 defer_sock "$SOCK69"
8791 SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock"
8792 defer_sock "$SOCK70"
8793 SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock"
8794 defer_sock "$SOCK71"
8795 SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock"
8796 defer_sock "$SOCK72"
8797 SOCK73="${TMPDIR:-/tmp}/muxd-e2e-stopgone-$$.sock"
8798 defer_sock "$SOCK73"
8799 # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from
8800 # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken
8801 # for by the two browser ports. A collision here reads as this leg's daemon
8802 # failing to bind, the same verdict every other band gives.
8803 UPQPORT=$(( 2000 + ($$ % 4000) ))
8804 UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$"
8805 defer_rm "$UPKEY"
8806 # A HOME of its own for the session shell. The title assertion below is
8807 # about a title the SESSION set, and a distribution's rc file repaints the
8808 # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says
8809 # so of Arch's) — read out of the developer's own dotfiles, this leg would
8810 # assert on whatever their prompt happens to spell.
8811 UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$"
8812 defer_rm "$UPHOME"
8813 # The candidate binary the refusal leg offers: a copy, so its exec bit can
8814 # be taken away without touching the one every other scenario runs.
8815 UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$"
8816 defer_rm "$UPBIN"
8817 UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock"
8818 defer_rm "$UPAGENT"
8819 UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$"
8820 defer_rm "$UPAGKEY" "$UPAGKEY.pub"
8846 8821
8847 # --- muxd upgrade: the daemon becomes the new binary, holding everything --- 8822 # --- muxd upgrade: the daemon becomes the new binary, holding everything ---
8848 # 8823 #
@@ -9377,6 +9352,50 @@ grep -q '^muxd: stopped' "$OUT.sg.stop" || {
9377 D73PID="" 9352 D73PID=""
9378 ok "muxd stop returns when the process is gone, not when the socket is" 9353 ok "muxd stop returns when the process is gone, not when the socket is"
9379 9354
9355 # The pins. Literals, not variables set from counting something else —
9356 # "assert the literal, never the constant the code under test reads"
9357 # (decisions.md, M10). The two figures live at the END of this file and
9358 # nowhere else: a count repeated in prose is a count nothing gates, and
9359 # both of the ones that used to stand here had drifted away from the pins
9360 # they described. Adding a scenario means editing those two literals by
9361 # hand, on purpose.
9362 #
9363 # Why most legs carry no convergence point. `assert_converged` replays one
9364 # client capture into an engine and diffs it against one daemon grid, so it
9365 # can only speak where BOTH exist and the grid is the subject. It says
9366 # nothing about a side channel (OSC 52, a window title, pty modes, a
9367 # fingerprint ssh printed, bytes counted rather than read), about a file an
9368 # editor or the wall wrote, about an exit code or a status field, about
9369 # WHICH of two sessions a keystroke reached, about scrollback the live grid
9370 # does not hold, about the absence of bytes two agreeing empties would
9371 # agree on anyway, or about a leg that never attaches at all. It also does
9372 # not claim to reproduce a full-screen application's redraws byte for byte,
9373 # which is why the alternate-screen legs stay out. A leg that asserts on
9374 # one of those and adds a convergence point would be pinning a fact every
9375 # leg above already establishes.
9376
9377 # The pins. Literals, not variables set from counting something else —
9378 # "assert the literal, never the constant the code under test reads"
9379 # (decisions.md, M10). The two figures live at the END of this file and
9380 # nowhere else: a count repeated in prose is a count nothing gates, and
9381 # both of the ones that used to stand here had drifted away from the pins
9382 # they described. Adding a scenario means editing those two literals by
9383 # hand, on purpose.
9384 #
9385 # Why most legs carry no convergence point. `assert_converged` replays one
9386 # client capture into an engine and diffs it against one daemon grid, so it
9387 # can only speak where BOTH exist and the grid is the subject. It says
9388 # nothing about a side channel (OSC 52, a window title, pty modes, a
9389 # fingerprint ssh printed, bytes counted rather than read), about a file an
9390 # editor or the wall wrote, about an exit code or a status field, about
9391 # WHICH of two sessions a keystroke reached, about scrollback the live grid
9392 # does not hold, about the absence of bytes two agreeing empties would
9393 # agree on anyway, or about a leg that never attaches at all. It also does
9394 # not claim to reproduce a full-screen application's redraws byte for byte,
9395 # which is why the alternate-screen legs stay out. A leg that asserts on
9396 # one of those and adds a convergence point would be pinning a fact every
9397 # leg above already establishes.
9398
9380 [ "$OK_COUNT" = "82" ] || { 9399 [ "$OK_COUNT" = "82" ] || {
9381 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 82 —" 9400 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 82 —"
9382 echo " a scenario was added (update the pin) or silently lost" 9401 echo " a scenario was added (update the pin) or silently lost"