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.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)" |