a73x

56781f44

test: e2e reads the planted env out of a shell and pins that no client was made

a73x   2026-08-19 17:17

Commit message
test: e2e reads the planted env out of a shell and pins that no client was made

Three claims in one leg. The shell does the comparing — `[ "$MUX_SOCK" = ...
]` — so a pass means the child saw the path this block bound rather than
that the script can rebuild a string. Then `mux --sock` is injected into
that shell: the refusal message, exit 2, and the assertion the leg exists
for, `"alt_screen":false` afterwards. Before this milestone that read true,
because an inner client really had taken the screen; a message grep alone
would still pass for a client that printed the line and attached anyway.
Last, `mux wall` naming the tile it refused.

No ptyclient: a real client on a real pty is what the refusal prevents, so
injecting one into the session shell IS the test, and the leg needs no
expect timing at all. The daemon runs 200 columns wide because every
assertion greps a message out of a grid, and at 80 the wall refusal — whose
length follows $TMPDIR — split mid-sentence and read as a missing one.

test/e2e.sh
Old New
@@ -272,6 +272,13 @@ D26PID=""
272 SOCK30="${TMPDIR:-/tmp}/muxd-e2e-wallchord-$$.sock" 272 SOCK30="${TMPDIR:-/tmp}/muxd-e2e-wallchord-$$.sock"
273 D27PID="" 273 D27PID=""
274 274
275 # M5 (the self-attach refusal): its own daemon because the assertion reads
276 # the SOCKET PATH out of a session shell's environment and compares it to
277 # the one this block started — a shared daemon would make the comparison a
278 # tautology about whichever block bound first.
279 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
280 D28PID=""
281
275 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 282 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
276 # such line on exit; every field is a key=value pair, so a rename or reorder 283 # such line on exit; every field is a key=value pair, so a rename or reorder
277 # in the client shows up here as an empty read rather than a wrong number. 284 # in the client shows up here as an empty read rather than a wrong number.
@@ -796,6 +803,7 @@ cleanup() {
796 [ -n "$D25PID" ] && kill "$D25PID" 2>/dev/null || true 803 [ -n "$D25PID" ] && kill "$D25PID" 2>/dev/null || true
797 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true 804 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true
798 [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true 805 [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true
806 [ -n "$D28PID" ] && kill "$D28PID" 2>/dev/null || true
799 # The stops still precede the socket rm below, like SOCK14-17 above: 807 # The stops still precede the socket rm below, like SOCK14-17 above:
800 # unlinking a socket first would leave a live daemon nothing could reach 808 # unlinking a socket first would leave a live daemon nothing could reach
801 # by path. 809 # by path.
@@ -808,6 +816,7 @@ cleanup() {
808 [ -S "$SOCK28" ] && "$MUXD" stop --sock "$SOCK28" 2>/dev/null || true 816 [ -S "$SOCK28" ] && "$MUXD" stop --sock "$SOCK28" 2>/dev/null || true
809 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true 817 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true
810 [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true 818 [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true
819 [ -S "$SOCK31" ] && "$MUXD" stop --sock "$SOCK31" 2>/dev/null || true
811 820
812 # ---- the leak sweep (hygiene kit, 6a) ---- 821 # ---- the leak sweep (hygiene kit, 6a) ----
813 # Here rather than at the bottom of the file, which `set -e` reaches only 822 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -820,7 +829,7 @@ cleanup() {
820 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ 829 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \
821 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ 830 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \
822 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 831 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \
823 "$D26PID" "$D27PID" 832 "$D26PID" "$D27PID" "$D28PID"
824 _leak=0 833 _leak=0
825 leak_sweep "$_rc" || _leak=1 834 leak_sweep "$_rc" || _leak=1
826 835
@@ -4784,6 +4793,93 @@ assert_stopped "$SOCK30" "$D27PID" "wall chord" "$OUT.wcstop"
4784 D27PID="" 4793 D27PID=""
4785 ok "Ctrl-\\ w: the wall shows both sessions, and q gives the session back" 4794 ok "Ctrl-\\ w: the wall shows both sessions, and q gives the session back"
4786 4795
4796 # ---- M5: a session shell knows where it is, and mux refuses the self-loop --
4797 #
4798 # The incident: `mux --sock S` typed in a shell of session 0 on S attached
4799 # that session to itself. Paint became a delta became a repaint, the inner
4800 # client took the alternate screen, and it ate every keystroke — and since
4801 # Ctrl-\ became a prefix the OUTER keyboard cannot steer an inner client
4802 # back out. There is no escape chord to offer, so the loop is refused.
4803 #
4804 # Three claims, in the order that makes each one's failure legible:
4805 #
4806 # 1. The daemon's identity reached a REAL shell. The SHELL does the
4807 # comparing — `[ "$MUX_SOCK" = ... ]` — so a pass means the child saw
4808 # the path this block bound, not that the script can rebuild a string.
4809 # 2. The refusal fires on the self-pair, and the loop never starts. The
4810 # money assertion is `"alt_screen":false` AFTER the injection: before
4811 # this milestone that read true, because an inner client really had
4812 # taken the screen. A message grep alone would still pass if the client
4813 # printed the line and attached anyway.
4814 # 3. `mux wall` refuses a tile that is the session it is running in, by
4815 # name. A wall that silently dropped it would be a wall that lies.
4816 #
4817 # Every marker is assembled by printf from pieces and never typed whole, so
4818 # the grid's echo of the command line can never satisfy the grep. `; echo`
4819 # rather than a `\n` inside printf's format: muxa's send turns every `\n`
4820 # into a real newline, so a format string carrying one would put a shell
4821 # continuation prompt in the middle of the assertion.
4822 #
4823 # No ptyclient here on purpose — a real client on a real pty is what the
4824 # refusal PREVENTS, so the leg that injects it into the session shell is the
4825 # leg that tests it. This block therefore needs no expect timing at all.
4826 #
4827 # 200 columns, and no client ever attaches to narrow it: every assertion
4828 # here greps a MESSAGE out of a grid dump, and a grid wraps. At 80 the wall
4829 # refusal — which names the spelling, so its length follows $TMPDIR — split
4830 # mid-sentence and a correct refusal read as a missing one. Width is the fix
4831 # that does not make the needle shorter than the claim.
4832 MUXABS=$(cd "$(dirname "$MUX")" && pwd)/$(basename "$MUX")
4833 "$MUXD" run --sock "$SOCK31" --shell /bin/sh --cols 200 > "$OUT.sa.d" 2>&1 &
4834 D28PID=$!
4835 wait_sock "$SOCK31" "$OUT.sa.d" "self-attach daemon never bound"
4836
4837 timeout 20 "$MUXA" send \
4838 "[ \"\$MUX_SOCK\" = \"$SOCK31\" ] && printf 'ENV%s-%s' OK \"\$MUX_SESSION\"; echo\n" \
4839 --sock "$SOCK31" --session 0 > "$OUT.saenv" 2>&1
4840 sleep 1
4841 timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap1" 2>&1
4842 # ENVOK-0 is both halves at once: the socket matched, and the name the shell
4843 # was told is the RESOLVED default, not the empty spelling the wire uses.
4844 grep -q "ENVOK-0" "$OUT.sacap1" || {
4845 echo "e2e FAIL: self-attach: the session shell was not told MUX_SOCK=$SOCK31 and MUX_SESSION=0:"
4846 cat "$OUT.saenv"; cat "$OUT.sacap1"; exit 1; }
4847
4848 timeout 20 "$MUXA" send \
4849 "$MUXABS --sock $SOCK31; printf 'SELF%s-%s' rc \$?; echo\n" \
4850 --sock "$SOCK31" --session 0 > "$OUT.saself" 2>&1
4851 sleep 2
4852 timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap2" 2>&1
4853 grep -q "unset MUX_SESSION to override" "$OUT.sacap2" || {
4854 echo "e2e FAIL: self-attach: mux did not refuse the session it was running in:"
4855 cat "$OUT.saself"; cat "$OUT.sacap2"; exit 1; }
4856 # Exit 2, the usage-error code every other refusal in this binary uses, and
4857 # proof the process ENDED rather than sitting there attached.
4858 grep -q "SELFrc-2" "$OUT.sacap2" || {
4859 echo "e2e FAIL: self-attach: the refusal did not exit 2 (or did not exit):"
4860 cat "$OUT.sacap2"; exit 1; }
4861 timeout 20 "$MUXA" status --sock "$SOCK31" --session 0 > "$OUT.sast" 2>&1
4862 grep -q '"alt_screen":false' "$OUT.sast" || {
4863 echo "e2e FAIL: self-attach: an inner client took the alternate screen — the loop ran:"
4864 cat "$OUT.sast"; exit 1; }
4865
4866 # The wall's spelling is ONE argument, quotes and all (the wall grammar,
4867 # wall.zig) — the same string scenario 33 puts in the state file.
4868 timeout 20 "$MUXA" send \
4869 "$MUXABS wall '--sock $SOCK31#0'; printf 'WALL%s-%s' rc \$?; echo\n" \
4870 --sock "$SOCK31" --session 0 > "$OUT.sawall" 2>&1
4871 sleep 2
4872 timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap3" 2>&1
4873 grep -q -- "mux: wall target '--sock $SOCK31#0' is the session this shell is inside" "$OUT.sacap3" || {
4874 echo "e2e FAIL: self-attach: mux wall struck its own tile without naming it:"
4875 cat "$OUT.sawall"; cat "$OUT.sacap3"; exit 1; }
4876 grep -q "WALLrc-2" "$OUT.sacap3" || {
4877 echo "e2e FAIL: self-attach: the wall refusal did not exit 2 (or did not exit):"
4878 cat "$OUT.sacap3"; exit 1; }
4879 assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop"
4880 D28PID=""
4881 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself"
4882
4787 # The long-lived daemon has served every scenario that wanted it; stop it 4883 # The long-lived daemon has served every scenario that wanted it; stop it
4788 # NOW so its allocator verdict is written while the suite is still running 4884 # NOW so its allocator verdict is written while the suite is still running
4789 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain 4885 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
@@ -4799,7 +4895,7 @@ DPID=""
4799 4895
4800 # The pins. Literals, not variables set from counting something else — 4896 # The pins. Literals, not variables set from counting something else —
4801 # "assert the literal, never the constant the code under test reads" 4897 # "assert the literal, never the constant the code under test reads"
4802 # (decisions.md, M10). 37 scenario checkpoints; 35 convergence points. 4898 # (decisions.md, M10). 38 scenario checkpoints; 35 convergence points.
4803 # Anyone adding a scenario updates these by hand, on purpose. 4899 # Anyone adding a scenario updates these by hand, on purpose.
4804 # 4900 #
4805 # M18 added three checkpoints and no convergence points: its wall block 4901 # M18 added three checkpoints and no convergence points: its wall block
@@ -4833,9 +4929,12 @@ DPID=""
4833 # time: it compares THREE sessions' grids against each other. The 37th is 4929 # time: it compares THREE sessions' grids against each other. The 37th is
4834 # Ctrl-\ w, and no convergence point for the CLI wall's reason: what it 4930 # Ctrl-\ w, and no convergence point for the CLI wall's reason: what it
4835 # asserts on is a ptyclient capture holding two sessions' bytes at once, 4931 # asserts on is a ptyclient capture holding two sessions' bytes at once,
4836 # which no dump of one grid can witness. 4932 # which no dump of one grid can witness. The 38th is the self-attach
4837 [ "$OK_COUNT" = "37" ] || { 4933 # refusal, and no convergence point because it has no client to converge:
4838 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 37 —" 4934 # the leg exists to prove no client was ever created, and its evidence is a
4935 # status field and a shell's exit code, neither of which is a grid.
4936 [ "$OK_COUNT" = "38" ] || {
4937 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 38 —"
4839 echo " a scenario was added (update the pin) or silently lost" 4938 echo " a scenario was added (update the pin) or silently lost"
4840 exit 1 4939 exit 1
4841 } 4940 }
@@ -4843,4 +4942,4 @@ DPID=""
4843 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" 4942 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
4844 exit 1 4943 exit 1
4845 } 4944 }
4846 echo "e2e OK (37 scenarios, 35 convergence points)" 4945 echo "e2e OK (38 scenarios, 35 convergence points)"