69a8393b
test: a wall polling two boxes starts no daemon; Enter and a cold mux HOST each start one
a73x 2026-08-28 19:53
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -177,8 +177,8 @@ done | |||
| 177 | # one of those and adds a convergence point would be pinning a fact every | 177 | # one of those and adds a convergence point would be pinning a fact every |
| 178 | # leg above already establishes. | 178 | # leg above already establishes. |
| 179 | 179 | ||
| 180 | [ "$OK_COUNT" = "86" ] || { | 180 | [ "$OK_COUNT" = "87" ] || { |
| 181 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 86 —" | 181 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 87 —" |
| 182 | echo " a scenario was added (update the pin) or silently lost" | 182 | echo " a scenario was added (update the pin) or silently lost" |
| 183 | exit 1 | 183 | exit 1 |
| 184 | } | 184 | } |
test/e2e_04_handoff.sh
| Old | New | ||
|---|---|---|---|
| @@ -62,18 +62,15 @@ HDEADPORT=$(( 16000 + ($$ % 4000) )) | |||
| 62 | # command was plain argv; it execs a program literally named | 62 | # command was plain argv; it execs a program literally named |
| 63 | # `PATH=... muxd endpoint` now, and dies at 127 without an announce. | 63 | # `PATH=... muxd endpoint` now, and dies at 127 without an announce. |
| 64 | # | 64 | # |
| 65 | # The arity guard covers the case that would NOT be loud: `ssh HOST` with | 65 | # The option loop and the arity guard come from `ssh_shim_head`, which |
| 66 | # no command at all. There, `shift` empties "$@" and a bare `exec` is a | 66 | # both of this suite's fake sshs share: the recipes these scenarios drive |
| 67 | # no-op, so the shim would exit 0 having run nothing — silence that looks | 67 | # happen to be the asking ones, which pass no `-o`, and a shim that is |
| 68 | # exactly like success. Note what the guard's 97 does and does not buy: | 68 | # right only by that coincidence breaks as "no announce" the day the entry |
| 69 | # nothing prints it, and the client experiences it only as an ssh that | 69 | # dial gains a flag. |
| 70 | # closed without an announce. It converts "quietly wrong" into "visibly | ||
| 71 | # no announce", which is a diagnosis this suite can act on. | ||
| 72 | mkdir -p "$SSHIM_DIR" "$HRUN" "$HRUN2" | 70 | mkdir -p "$SSHIM_DIR" "$HRUN" "$HRUN2" |
| 73 | : > "$SSHIM_PIDLOG" | 71 | : > "$SSHIM_PIDLOG" |
| 74 | cat > "$SSHIM_DIR/ssh" <<'SHIM' | 72 | ssh_shim_head "$SSHIM_DIR/ssh" |
| 75 | #!/bin/sh | 73 | cat >> "$SSHIM_DIR/ssh" <<'SHIM' |
| 76 | [ $# -ge 2 ] || exit 97 | ||
| 77 | echo $$ >> "${SSHIM_PIDLOG:?}" | 74 | echo $$ >> "${SSHIM_PIDLOG:?}" |
| 78 | shift | 75 | shift |
| 79 | exec /bin/sh -c "$*" | 76 | exec /bin/sh -c "$*" |
test/e2e_09_hosts.sh
| Old | New | ||
|---|---|---|---|
| @@ -808,3 +808,168 @@ PK1PID="" | |||
| 808 | assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop" | 808 | assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop" |
| 809 | PK2PID="" | 809 | PK2PID="" |
| 810 | ok "the picker births on the host a digit names, forgets a host without ending it, adds one back, and gives the keyboard back" | 810 | ok "the picker births on the host a digit names, forgets a host without ending it, adds one back, and gives the keyboard back" |
| 811 | |||
| 812 | # ---- reading a host never starts a daemon; asking does ------------------ | ||
| 813 | # | ||
| 814 | # `muxd endpoint` used to auto-start a daemon before answering, and the | ||
| 815 | # wall runs it over ssh once a second per listed host. A listed box with | ||
| 816 | # nothing on it therefore got a daemon — and a shell in session 0 — from a | ||
| 817 | # READ, and a `muxd stop` typed on that box was undone a second later. | ||
| 818 | # | ||
| 819 | # The rule now: a daemon starts when somebody asks. `mux HOST` and the | ||
| 820 | # picker's Enter ask, and answer a refusal by running `muxd start` over the | ||
| 821 | # same ssh; the poller, `mux hosts` and every redial report `unreachable` | ||
| 822 | # and leave the box alone. | ||
| 823 | # | ||
| 824 | # The oracle is the OS, never the daemon: the socket INODE on the far side, | ||
| 825 | # and a walk of /proc for a `muxd run` born under that box's runtime dir. A | ||
| 826 | # daemon asked whether it exists cannot answer no. | ||
| 827 | # | ||
| 828 | # Two fake boxes, because one cannot show that a birth landed on the row | ||
| 829 | # the user chose: the shim reads the host word and hands the remote command | ||
| 830 | # a runtime dir of that box's own, so `muxd`'s default socket resolves to a | ||
| 831 | # different path per host — which is exactly what two machines are. | ||
| 832 | NSHIM="${TMPDIR:-/tmp}/mux-e2e-nostart-shim-$$" | ||
| 833 | defer_rm "$NSHIM" | ||
| 834 | NSTATE="${TMPDIR:-/tmp}/mux-e2e-nostart-state-$$" | ||
| 835 | defer_rm "$NSTATE" | ||
| 836 | NRUNA="${TMPDIR:-/tmp}/mux-e2e-nostart-a-$$" | ||
| 837 | defer_rm "$NRUNA" | ||
| 838 | NRUNB="${TMPDIR:-/tmp}/mux-e2e-nostart-b-$$" | ||
| 839 | defer_rm "$NRUNB" | ||
| 840 | NSOCKA="$NRUNA/muxd.sock" | ||
| 841 | NSOCKB="$NRUNB/muxd.sock" | ||
| 842 | defer_sock "$NSOCKA" "$NSOCKB" | ||
| 843 | NSHIMLOG="$NSHIM/hosts" | ||
| 844 | export NRUNA NRUNB NSHIMLOG | ||
| 845 | mkdir -p "$NSHIM" "$NRUNA" "$NRUNB" "$NSTATE/mux" | ||
| 846 | : > "$NSHIMLOG" | ||
| 847 | # The option loop and the arity guard are `ssh_shim_head`'s; what is this | ||
| 848 | # leg's own is the runtime dir per host word, which is what makes two fake | ||
| 849 | # boxes two machines — `muxd`'s default socket resolves to a different path | ||
| 850 | # on each. | ||
| 851 | ssh_shim_head "$NSHIM/ssh" | ||
| 852 | cat >> "$NSHIM/ssh" <<'SHIM' | ||
| 853 | case "$1" in | ||
| 854 | nostart-a@*) XDG_RUNTIME_DIR="${NRUNA:?}" ;; | ||
| 855 | nostart-b@*) XDG_RUNTIME_DIR="${NRUNB:?}" ;; | ||
| 856 | *) exit 96 ;; | ||
| 857 | esac | ||
| 858 | export XDG_RUNTIME_DIR | ||
| 859 | echo "$1" >> "${NSHIMLOG:?}" | ||
| 860 | shift | ||
| 861 | exec /bin/sh -c "$*" | ||
| 862 | SHIM | ||
| 863 | chmod +x "$NSHIM/ssh" | ||
| 864 | NPATH="$NSHIM:$(cd "$(dirname "$MUXD")" && pwd):$PATH" | ||
| 865 | printf 'nostart-a@127.0.0.1\nnostart-b@127.0.0.1\n' > "$NSTATE/mux/hosts" | ||
| 866 | |||
| 867 | # The /proc walk. `muxd run` and not `muxd` — the transient `muxd endpoint` | ||
| 868 | # and `muxd start` processes share the runtime dir and are not daemons — | ||
| 869 | # and `environ` rather than a name, because a daemon `muxd start` spawns | ||
| 870 | # carries no --sock to grep for: it took the default from this variable. | ||
| 871 | nostart_daemons() { | ||
| 872 | _nd=0 | ||
| 873 | for _ne in /proc/[0-9]*/environ; do | ||
| 874 | _np=${_ne#/proc/} | ||
| 875 | _np=${_np%/environ} | ||
| 876 | # The redirect itself is what fails on another user's process (and | ||
| 877 | # on one that exits mid-walk), so the silencing has to wrap the | ||
| 878 | # whole command and not just `tr`. | ||
| 879 | { tr '\0' '\n' < "$_ne"; } 2>/dev/null | grep -qxF "XDG_RUNTIME_DIR=$1" || continue | ||
| 880 | { tr '\0' ' ' < "/proc/$_np/cmdline"; } 2>/dev/null | grep -q 'muxd run' || continue | ||
| 881 | _nd=$((_nd + 1)) | ||
| 882 | done | ||
| 883 | echo "$_nd" | ||
| 884 | } | ||
| 885 | |||
| 886 | nostart_empty() { # $1 = runtime dir, $2 = socket, $3 = what was just done | ||
| 887 | _nc=$(nostart_daemons "$1") | ||
| 888 | [ "$_nc" -eq 0 ] || { | ||
| 889 | echo "e2e FAIL: no-start: $3 left $_nc muxd run under $1, want 0"; exit 1; } | ||
| 890 | [ ! -e "$2" ] || { | ||
| 891 | echo "e2e FAIL: no-start: $3 bound $2 — a read started a daemon"; exit 1; } | ||
| 892 | } | ||
| 893 | |||
| 894 | # (1) `mux hosts` READS every listed box. Both rows unreachable, both boxes | ||
| 895 | # untouched. | ||
| 896 | XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 40 "$MUX" hosts > "$OUT.nsh" 2>&1 || { | ||
| 897 | echo "e2e FAIL: no-start: mux hosts failed:"; cat "$OUT.nsh"; exit 1; } | ||
| 898 | [ "$(grep -c '\[unreachable\]' "$OUT.nsh")" = "2" ] || { | ||
| 899 | echo "e2e FAIL: no-start: mux hosts did not call both boxes unreachable:" | ||
| 900 | cat "$OUT.nsh"; exit 1; } | ||
| 901 | nostart_empty "$NRUNA" "$NSOCKA" "mux hosts" | ||
| 902 | nostart_empty "$NRUNB" "$NSOCKB" "mux hosts" | ||
| 903 | # The shim really ran, so the emptiness above is a refusal and not a leg | ||
| 904 | # that never dialled: one line per box, at least. | ||
| 905 | grep -q 'nostart-a@' "$NSHIMLOG" && grep -q 'nostart-b@' "$NSHIMLOG" || { | ||
| 906 | echo "e2e FAIL: no-start: mux hosts dialled neither box:"; cat "$NSHIMLOG"; exit 1; } | ||
| 907 | |||
| 908 | # (2) and (3) in one wall. The wall polls both boxes once a second — three | ||
| 909 | # seconds of that must change nothing — and then Enter on ROW 2 asks, which | ||
| 910 | # must give box B a daemon and box A none. Row 2 and not row 1: a birth | ||
| 911 | # that reached for the first row, or for the focus's host, would pass a | ||
| 912 | # one-row fixture and this one catches it. | ||
| 913 | set +e | ||
| 914 | SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 90 "$PTYCLIENT" \ | ||
| 915 | --cols 100 --rows 30 --out "$OUT.nscap" --err "$OUT.nscap.err" -- \ | ||
| 916 | "$MUX" > "$OUT.nspc" 2>&1 <<'EOF' | ||
| 917 | expect unreachable 25000 | ||
| 918 | settle 3000 25000 | ||
| 919 | send 2 | ||
| 920 | settle 500 15000 | ||
| 921 | send \r | ||
| 922 | settle 2000 30000 | ||
| 923 | send printf 'ns-born-%s\n' ok\n | ||
| 924 | expect ns-born-ok 25000 | ||
| 925 | send \x1cd | ||
| 926 | waitexit 15000 | ||
| 927 | EOF | ||
| 928 | RC=$? | ||
| 929 | set -e | ||
| 930 | [ "$RC" -eq 0 ] || { | ||
| 931 | echo "e2e FAIL: no-start: the picker leg exited $RC:" | ||
| 932 | cat "$OUT.nspc"; echo "--- stderr ---"; cat "$OUT.nscap.err"; exit 1; } | ||
| 933 | # Box A was polled for the whole run and never asked for: still nothing. | ||
| 934 | nostart_empty "$NRUNA" "$NSOCKA" "a wall polling it" | ||
| 935 | # Box B was chosen. EXACTLY one daemon — a start per backoff, or a start on | ||
| 936 | # every poll cycle, would show as several. | ||
| 937 | NCB=$(nostart_daemons "$NRUNB") | ||
| 938 | [ "$NCB" -eq 1 ] || { | ||
| 939 | echo "e2e FAIL: no-start: picker Enter left $NCB muxd run under $NRUNB, want 1"; exit 1; } | ||
| 940 | [ -S "$NSOCKB" ] || { | ||
| 941 | echo "e2e FAIL: no-start: picker Enter started nothing on box B ($NSOCKB)"; exit 1; } | ||
| 942 | # ...and it is a real session, named `0` off the daemon's own empty list. | ||
| 943 | dump_session "$NSOCKB" 0 | grep -q 'ns-born-ok' || { | ||
| 944 | echo "e2e FAIL: no-start: box B's session 0 never ran the marker:" | ||
| 945 | "$MUXD" stats --sock "$NSOCKB"; exit 1; } | ||
| 946 | |||
| 947 | # (4) A cold `mux HOST` entry, on the box the wall left alone. One daemon, | ||
| 948 | # and the client said so in its own voice — `muxd start`'s progress reaches | ||
| 949 | # the user's terminal over the same inherited stderr ssh's own diagnostics | ||
| 950 | # ride on. | ||
| 951 | pipe_mux "$OUT.nsa" "$OUT.nsa.err" env SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" \ | ||
| 952 | PATH="$NPATH" timeout 60 "$MUX" "nostart-a@127.0.0.1" | ||
| 953 | pipe_send 'printf "ns-cold-%%s\\n" ok\n' | ||
| 954 | await_out "$OUT.nsa" "ns-cold-ok" "no-start: the cold entry never served a session" | ||
| 955 | pipe_detach "no-start cold entry client" | ||
| 956 | NCA=$(nostart_daemons "$NRUNA") | ||
| 957 | [ "$NCA" -eq 1 ] || { | ||
| 958 | echo "e2e FAIL: no-start: a cold mux HOST left $NCA muxd run under $NRUNA, want 1" | ||
| 959 | cat "$OUT.nsa.err"; exit 1; } | ||
| 960 | [ -S "$NSOCKA" ] || { | ||
| 961 | echo "e2e FAIL: no-start: the cold entry started nothing on box A"; exit 1; } | ||
| 962 | [ "$(grep -c '^muxd: starting' "$OUT.nsa.err")" = "1" ] || { | ||
| 963 | echo "e2e FAIL: no-start: the cold entry did not run muxd start exactly once:" | ||
| 964 | cat "$OUT.nsa.err"; exit 1; } | ||
| 965 | # ...and box A goes down by the daemon's own verb, which makes the teardown | ||
| 966 | # one more assertion rather than a daemon handed to the trap: what a `muxd | ||
| 967 | # stop` owes on a box somebody started is exactly what an untouched box | ||
| 968 | # already shows — no `muxd run` under that runtime dir, and no socket. | ||
| 969 | # `assert_stopped` is not usable here: it wants a pid, and this daemon was | ||
| 970 | # forked by an ssh shim on the far side of a client. | ||
| 971 | "$MUXD" stop --sock "$NSOCKA" > "$OUT.nsastop" 2>&1 || { | ||
| 972 | echo "e2e FAIL: no-start: muxd stop on box A exited nonzero:" | ||
| 973 | cat "$OUT.nsastop"; exit 1; } | ||
| 974 | nostart_empty "$NRUNA" "$NSOCKA" "muxd stop" | ||
| 975 | ok "reading a host leaves it alone; a picker Enter and a cold mux HOST each start one daemon" | ||
test/e2e_lib.sh
| Old | New | ||
|---|---|---|---|
| @@ -464,6 +464,36 @@ repaints() { | |||
| 464 | grep -a -F -o "$_clr" "$1" 2>/dev/null | wc -l | 464 | grep -a -F -o "$_clr" "$1" 2>/dev/null | wc -l |
| 465 | } | 465 | } |
| 466 | 466 | ||
| 467 | # ssh_shim_head FILE — the prologue every fake ssh in this suite needs, | ||
| 468 | # written fresh over FILE; the caller appends its own body and chmods. | ||
| 469 | # | ||
| 470 | # Real ssh takes options before the host word, and the client spells some: | ||
| 471 | # `handoff.recipeFor` adds `-o BatchMode=yes` to every recipe that must not | ||
| 472 | # ask, and the wall's poller runs those. A shim that read `$1` as the host | ||
| 473 | # would exec `BatchMode=yes ...` and fail exactly like an unreachable box — | ||
| 474 | # the state the no-start legs assert, so the fixture would be lying in the | ||
| 475 | # direction of a pass. The arity guard is the other half: `ssh HOST` with | ||
| 476 | # no command leaves `"$@"` empty after the shift, and a bare exec there is | ||
| 477 | # a no-op that exits 0 — silence shaped like success. 97 converts that into | ||
| 478 | # a visible "no announce". | ||
| 479 | # | ||
| 480 | # Here, not per group, because the coupling was invisible: the option loop | ||
| 481 | # is correct in one shim and absent in the other only because those legs | ||
| 482 | # happen to drive asking recipes, and nothing said so. | ||
| 483 | ssh_shim_head() { | ||
| 484 | cat > "$1" <<'SHIMHEAD' | ||
| 485 | #!/bin/sh | ||
| 486 | while [ $# -gt 0 ]; do | ||
| 487 | case "$1" in | ||
| 488 | -o) shift 2 ;; | ||
| 489 | -*) shift ;; | ||
| 490 | *) break ;; | ||
| 491 | esac | ||
| 492 | done | ||
| 493 | [ $# -ge 2 ] || exit 97 | ||
| 494 | SHIMHEAD | ||
| 495 | } | ||
| 496 | |||
| 467 | # rail_cols FILE — the column of every vertical rail in a capture, | 497 | # rail_cols FILE — the column of every vertical rail in a capture, |
| 468 | # ascending and deduplicated. A rail paints as a CUP followed by reverse | 498 | # ascending and deduplicated. A rail paints as a CUP followed by reverse |
| 469 | # video, which is how a label bar starts too, so column 1 is a bar and | 499 | # video, which is how a label bar starts too, so column 1 is a bar and |