88bcfd7a
test: e2e — the wall of hosts (two daemons, x two-step, restart re-creates nothing)
a73x 2026-08-28 19:53
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -112,7 +112,7 @@ command -v python3 > /dev/null 2>&1 || { | |||
| 112 | E2E_DIR=$(dirname "$0") | 112 | E2E_DIR=$(dirname "$0") |
| 113 | . "$E2E_DIR/e2e_lib.sh" | 113 | . "$E2E_DIR/e2e_lib.sh" |
| 114 | 114 | ||
| 115 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_wallhist 10_agent 11_select 12_panes 13_birth 14_upgrade' | 115 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_hosts 10_agent 11_select 12_panes 13_birth 14_upgrade' |
| 116 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses | 116 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses |
| 117 | # these by name rather than running them: a group whose fixtures are | 117 | # these by name rather than running them: a group whose fixtures are |
| 118 | # missing does not fail, it passes having tested something else — and a | 118 | # missing does not fail, it passes having tested something else — and a |
test/e2e_09_hosts.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,489 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_09_hosts.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in | ||
| 3 | # the order they stand in; see the lib's header for what this file may | ||
| 4 | # assume and what it must register. | ||
| 5 | # | ||
| 6 | # The wall is a list of DAEMONS: two of them here, because the claim is | ||
| 7 | # that the tiles come from the daemons and not from a file, and one daemon | ||
| 8 | # cannot show which of the two a tile belongs to. Both daemons have a | ||
| 9 | # session named `0` for exactly that reason — a wall that keyed tiles on | ||
| 10 | # the session name alone would show one of them twice. | ||
| 11 | # | ||
| 12 | # A state home of its own, and one per leg that reads the file back: every | ||
| 13 | # `mux` in this suite now records the DAEMON it attached to, so a hosts | ||
| 14 | # file read out of the shared $XDG_STATE_HOME would be every other group's | ||
| 15 | # attaches as much as this one's. | ||
| 16 | SOCKH1="${TMPDIR:-/tmp}/muxd-e2e-hosts1-$$.sock" | ||
| 17 | defer_sock "$SOCKH1" | ||
| 18 | SOCKH2="${TMPDIR:-/tmp}/muxd-e2e-hosts2-$$.sock" | ||
| 19 | defer_sock "$SOCKH2" | ||
| 20 | HSTATE="${TMPDIR:-/tmp}/mux-e2e-hosts-state-$$" | ||
| 21 | defer_rm "$HSTATE" | ||
| 22 | # The auto-start leg: a state home whose hosts file NAMES the local daemon, | ||
| 23 | # and a runtime dir of its own so `--sock <default>` resolves to a path | ||
| 24 | # nothing in this suite — or on this developer's machine — is already | ||
| 25 | # listening on. Without the second, the leg would start (or worse, adopt) | ||
| 26 | # the daemon the user is sitting in. | ||
| 27 | HASTATE="${TMPDIR:-/tmp}/mux-e2e-hosts-auto-$$" | ||
| 28 | defer_rm "$HASTATE" | ||
| 29 | HARUN="${TMPDIR:-/tmp}/mux-e2e-hosts-run-$$" | ||
| 30 | defer_rm "$HARUN" | ||
| 31 | HASOCK="$HARUN/muxd.sock" | ||
| 32 | defer_sock "$HASOCK" | ||
| 33 | # The `x` leg builds and eats its own daemon's session, so it reads a wall | ||
| 34 | # of ONE host and gets a state home nothing else writes. | ||
| 35 | HXSTATE="${TMPDIR:-/tmp}/mux-e2e-hosts-x-state-$$" | ||
| 36 | defer_rm "$HXSTATE" | ||
| 37 | |||
| 38 | start_daemon "$SOCKH1" "$OUT.h1.d" "hosts daemon 1 never bound" --shell /bin/sh | ||
| 39 | DH1PID=$DPID | ||
| 40 | start_daemon "$SOCKH2" "$OUT.h2.d" "hosts daemon 2 never bound" --shell /bin/sh | ||
| 41 | DH2PID=$DPID | ||
| 42 | |||
| 43 | # hosts_where NEEDLE — which of the four live sessions on the two daemons | ||
| 44 | # hold NEEDLE, one `SOCK:NAME` per line. The wall's tiles are read back | ||
| 45 | # from the DAEMONS that own them, never from the terminal: a marker on | ||
| 46 | # screen says a stripe painted, and what these legs claim is which SESSION | ||
| 47 | # a keystroke reached. | ||
| 48 | hosts_where() { | ||
| 49 | for _hw in "$SOCKH1:0" "$SOCKH1:b" "$SOCKH2:0" "$SOCKH2:c"; do | ||
| 50 | if dump_session "${_hw%%:*}" "${_hw##*:}" 2>/dev/null | grep -q "$1"; then | ||
| 51 | printf '%s\n' "$_hw" | ||
| 52 | fi | ||
| 53 | done | ||
| 54 | } | ||
| 55 | |||
| 56 | # ---- the wall records DAEMONS, and starts a local one that is not there -- | ||
| 57 | # | ||
| 58 | # Two halves of the same rule, and the file is the witness for both. | ||
| 59 | # | ||
| 60 | # * `mux --sock PATH` records the line `--sock PATH` — the DAEMON — once | ||
| 61 | # its dial has come up. Never `#0`: a wall line names nothing that can | ||
| 62 | # be resurrected, so a `#` anywhere in this file is the regression. | ||
| 63 | # * a line naming the LOCAL daemon is auto-started when nothing answers | ||
| 64 | # on it. That line outlives every reboot while the daemon does not, and | ||
| 65 | # a wall that paints the user's own machine `[unreachable]` until they | ||
| 66 | # find some other shell to start a daemon in is the empty-file case | ||
| 67 | # with one line in front of it. | ||
| 68 | # | ||
| 69 | # The auto-start half is asked of the OS, not of the wall: the marker is | ||
| 70 | # the SHELL's own `$$`, and `muxd stats` on the default path is a second | ||
| 71 | # process finding a daemon that was not there a moment ago. | ||
| 72 | pipe_mux "$OUT.hw1" "$OUT.hw1.err" env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" --sock "$SOCKH1" | ||
| 73 | pipe_send 'printf "hw1-%%s\\n" pin\n' | ||
| 74 | await_out "$OUT.hw1" "hw1-pin" "hw1-pin never reached the client" | ||
| 75 | pipe_detach "hosts client 1" | ||
| 76 | wait_grid "$SOCKH1" "hw1-pin" "hosts: the default session's marker" | ||
| 77 | grep -qxF -- "--sock $SOCKH1" "$HSTATE/mux/hosts" || { | ||
| 78 | echo "e2e FAIL: hosts: the attach recorded no daemon line:" | ||
| 79 | cat "$HSTATE/mux/hosts" 2>&1; exit 1; } | ||
| 80 | if grep -q '#' "$HSTATE/mux/hosts"; then | ||
| 81 | echo "e2e FAIL: hosts: a session leaked into the hosts file:" | ||
| 82 | cat "$HSTATE/mux/hosts"; exit 1 | ||
| 83 | fi | ||
| 84 | |||
| 85 | # The listed-local half. The file is written by hand rather than by an | ||
| 86 | # attach, because what is under test is a line whose daemon is GONE — the | ||
| 87 | # state every hosts file is in after a reboot. | ||
| 88 | mkdir -p "$HASTATE/mux" "$HARUN" | ||
| 89 | printf -- '--sock %s\n' "$HASOCK" > "$HASTATE/mux/hosts" | ||
| 90 | [ ! -S "$HASOCK" ] || { echo "e2e FAIL: hosts: the auto-start path is already served"; exit 1; } | ||
| 91 | set +e | ||
| 92 | # PATH so `findInPath` resolves exactly the muxd under test, and | ||
| 93 | # XDG_RUNTIME_DIR so `--sock <default>` is this leg's own path; the wall | ||
| 94 | # needs a terminal, so this is the pty fixture and not a pipe. | ||
| 95 | SHELL=/bin/sh PATH="$(dirname "$MUXD"):$PATH" XDG_RUNTIME_DIR="$HARUN" \ | ||
| 96 | XDG_STATE_HOME="$HASTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 24 \ | ||
| 97 | --out "$OUT.hauto" --err "$OUT.hauto.err" -- "$MUX" > "$OUT.hauto.log" 2>&1 <<'EOF' | ||
| 98 | expect \x1b[?1049h 20000 | ||
| 99 | settle 600 20000 | ||
| 100 | send echo hapid=$$\n | ||
| 101 | expect hapid= 20000 | ||
| 102 | settle 600 20000 | ||
| 103 | send \x1cd | ||
| 104 | waitexit 10000 | ||
| 105 | EOF | ||
| 106 | RC=$? | ||
| 107 | set -e | ||
| 108 | [ "$RC" -eq 0 ] || { | ||
| 109 | echo "e2e FAIL: hosts: the auto-start leg exited $RC (did the listed local daemon start?):" | ||
| 110 | cat "$OUT.hauto.log"; cat "$OUT.hauto.err" 2>/dev/null; exit 1; } | ||
| 111 | HAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.hauto.err" | head -1) | ||
| 112 | defer_kill "$HAPID" | ||
| 113 | [ -n "$HAPID" ] || { | ||
| 114 | echo "e2e FAIL: hosts: the auto-start printed no pid to hold onto:" | ||
| 115 | cat "$OUT.hauto.err"; exit 1; } | ||
| 116 | # The daemon exists and holds the shell that answered — asked of the daemon | ||
| 117 | # by a process that had nothing to do with starting it. | ||
| 118 | HASH_PID=$("$MUXD" dump --sock "$HASOCK" | sed -n 's/.*hapid=\([0-9]*\).*/\1/p' | tail -1) | ||
| 119 | [ -n "$HASH_PID" ] || { | ||
| 120 | echo "e2e FAIL: hosts: the auto-started daemon holds no shell pid:" | ||
| 121 | "$MUXD" dump --sock "$HASOCK"; exit 1; } | ||
| 122 | # ...and the pid on the grid is a process the OS agrees exists, in the | ||
| 123 | # daemon's own tree. A daemon reporting on itself cannot catch itself | ||
| 124 | # being wrong. | ||
| 125 | [ "$(ps -o ppid= -p "$HASH_PID" 2>/dev/null | tr -d ' ')" = "$(real_pid "$HAPID")" ] || { | ||
| 126 | echo "e2e FAIL: hosts: the shell on the grid is not the auto-started daemon's child" | ||
| 127 | ps -o pid,ppid,args -p "$HASH_PID" 2>&1; exit 1; } | ||
| 128 | assert_stopped "$HASOCK" "$HAPID" "hosts auto-start" "$OUT.hastop" | ||
| 129 | HAPID="" | ||
| 130 | ok "mux records the daemon on the wall, never a session, and starts a listed local one that is gone" | ||
| 131 | |||
| 132 | # ---- every live session of every listed daemon is a tile ---------------- | ||
| 133 | # | ||
| 134 | # The headline. Two daemons on the wall, four sessions between them, and | ||
| 135 | # the wall is asked for all four AT ONCE — on a real pty, because a stripe | ||
| 136 | # is a rect on a terminal and a pipe has none. | ||
| 137 | # | ||
| 138 | # Three claims: | ||
| 139 | # | ||
| 140 | # * four bars, and each names the daemon it belongs to. Both daemons have | ||
| 141 | # a session called `0`; a wall keyed on the session name would paint | ||
| 142 | # three tiles and call it four. | ||
| 143 | # * `Ctrl-\ n` walks them. Where the two markers LANDED is read off the | ||
| 144 | # daemons, so a step that only moved a `>` on screen fails here. | ||
| 145 | # * `Ctrl-\ c` births a session, and the file does not move. The wall is | ||
| 146 | # the daemons' truth: a second `mux` finds the new session as a tile | ||
| 147 | # with nothing written down anywhere. | ||
| 148 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts add "--sock $SOCKH2" > "$OUT.hadd" 2>&1 || { | ||
| 149 | echo "e2e FAIL: hosts: 'hosts add' refused the second daemon:" | ||
| 150 | cat "$OUT.hadd"; exit 1; } | ||
| 151 | pipe_mux "$OUT.hb" "$OUT.hb.err" env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" --sock "$SOCKH1" --session b | ||
| 152 | pipe_send 'printf "hb-%%s\\n" pin\n' | ||
| 153 | await_out "$OUT.hb" "hb-pin" "hb-pin never reached the client" | ||
| 154 | pipe_detach "hosts session b" | ||
| 155 | wait_grid "$SOCKH1" "hb-pin" "hosts: session b's marker" b | ||
| 156 | pipe_mux "$OUT.hc" "$OUT.hc.err" env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" --sock "$SOCKH2" --session c | ||
| 157 | pipe_send 'printf "hc-%%s\\n" pin\n' | ||
| 158 | await_out "$OUT.hc" "hc-pin" "hc-pin never reached the client" | ||
| 159 | pipe_detach "hosts session c" | ||
| 160 | wait_grid "$SOCKH2" "hc-pin" "hosts: session c's marker" c | ||
| 161 | wait_sessions "$SOCKH1" 2 "hosts: daemon 1 should hold 0 and b" | ||
| 162 | wait_sessions "$SOCKH2" 2 "hosts: daemon 2 should hold 0 and c" | ||
| 163 | |||
| 164 | # The file, byte for byte, across a `Ctrl-\ c` that creates a session: the | ||
| 165 | # wall's tiles come from the daemons, so a birth writes nothing down. | ||
| 166 | HFHASH=$(sha256sum "$HSTATE/mux/hosts" | cut -d' ' -f1) | ||
| 167 | set +e | ||
| 168 | # Tall and wide: five tiles (four sessions plus the one `Ctrl-\ c` makes) | ||
| 169 | # have to fit under the row floor a stripe's label bar leaves. | ||
| 170 | XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 44 \ | ||
| 171 | --out "$OUT.hwcap" --err "$OUT.hwcap.err" -- "$MUX" > "$OUT.hwpc" 2>&1 <<'EOF' | ||
| 172 | expect hc-pin 25000 | ||
| 173 | settle 1500 25000 | ||
| 174 | send printf 'walk-%s\\n' one\n | ||
| 175 | expect walk-one 20000 | ||
| 176 | settle 800 20000 | ||
| 177 | send \x1cn | ||
| 178 | settle 800 20000 | ||
| 179 | send printf 'walk-%s\\n' two\n | ||
| 180 | expect walk-two 20000 | ||
| 181 | settle 800 20000 | ||
| 182 | send \x1cc | ||
| 183 | settle 2500 25000 | ||
| 184 | send \x1cd | ||
| 185 | waitexit 15000 | ||
| 186 | EOF | ||
| 187 | RC=$? | ||
| 188 | set -e | ||
| 189 | [ "$RC" -eq 0 ] || { | ||
| 190 | echo "e2e FAIL: hosts: the wall leg exited $RC (did every daemon's sessions become tiles?):" | ||
| 191 | cat "$OUT.hwpc"; exit 1; } | ||
| 192 | # Four bars, each naming its own daemon. | ||
| 193 | for _hl in "$SOCKH1#0" "$SOCKH1#b" "$SOCKH2#0" "$SOCKH2#c"; do | ||
| 194 | grep -q -- "--sock $_hl \[up\]" "$OUT.hwcap" || { | ||
| 195 | echo "e2e FAIL: hosts: '--sock $_hl' never became a stripe on the wall:" | ||
| 196 | cat "$OUT.hwpc"; exit 1; } | ||
| 197 | done | ||
| 198 | # The walk, read off the daemons. One holder each, and not the same one: | ||
| 199 | # `n` moved the focus to a different SESSION, on whichever daemon owns it. | ||
| 200 | HW1=$(hosts_where walk-one) | ||
| 201 | HW2=$(hosts_where walk-two) | ||
| 202 | [ "$(printf '%s\n' "$HW1" | grep -c .)" = "1" ] || { | ||
| 203 | echo "e2e FAIL: hosts: 'walk-one' landed in [$HW1], want exactly one session"; exit 1; } | ||
| 204 | [ "$(printf '%s\n' "$HW2" | grep -c .)" = "1" ] || { | ||
| 205 | echo "e2e FAIL: hosts: 'walk-two' landed in [$HW2], want exactly one session"; exit 1; } | ||
| 206 | [ "$HW1" != "$HW2" ] || { | ||
| 207 | echo "e2e FAIL: hosts: Ctrl-\\ n did not move the focus — both markers landed in $HW1"; exit 1; } | ||
| 208 | # The birth wrote nothing down, and the daemon really has one more session. | ||
| 209 | [ "$HFHASH" = "$(sha256sum "$HSTATE/mux/hosts" | cut -d' ' -f1)" ] || { | ||
| 210 | echo "e2e FAIL: hosts: Ctrl-\\ c edited the hosts file — a birth is the daemon's, not the file's:" | ||
| 211 | cat "$HSTATE/mux/hosts"; exit 1; } | ||
| 212 | HBORN=$(( $("$MUXD" stats --sock "$SOCKH1" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p') + | ||
| 213 | $("$MUXD" stats --sock "$SOCKH2" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p') )) | ||
| 214 | [ "$HBORN" -eq 5 ] || { | ||
| 215 | echo "e2e FAIL: hosts: Ctrl-\\ c left $HBORN sessions across the two daemons, want 5" | ||
| 216 | "$MUXD" stats --sock "$SOCKH1"; "$MUXD" stats --sock "$SOCKH2"; exit 1; } | ||
| 217 | # ...and a SECOND wall finds it, with nothing having been written down. | ||
| 218 | set +e | ||
| 219 | XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 44 \ | ||
| 220 | --out "$OUT.hw2cap" --err "$OUT.hw2cap.err" -- "$MUX" > "$OUT.hw2pc" 2>&1 <<'EOF' | ||
| 221 | expect [up] 25000 | ||
| 222 | settle 2000 25000 | ||
| 223 | send \x1cd | ||
| 224 | waitexit 15000 | ||
| 225 | EOF | ||
| 226 | RC=$? | ||
| 227 | set -e | ||
| 228 | [ "$RC" -eq 0 ] || { | ||
| 229 | echo "e2e FAIL: hosts: the second wall exited $RC:"; cat "$OUT.hw2pc"; exit 1; } | ||
| 230 | # `1` is the name `nextFreeName` picks on either daemon — both hold `0` and | ||
| 231 | # one letter — so which host the birth landed on does not change what it is | ||
| 232 | # called, only which stripe carries it. | ||
| 233 | grep -q -- "#1 \[" "$OUT.hw2cap" || { | ||
| 234 | echo "e2e FAIL: hosts: the session Ctrl-\\ c created is not a tile on the next wall:" | ||
| 235 | cat "$OUT.hw2pc"; exit 1; } | ||
| 236 | # ...and then it goes again, so the counts the legs below assert are the | ||
| 237 | # ones this file set up. WHICH of the two hosts the birth landed on depends | ||
| 238 | # on which polled first, and that is a race no leg here has any business | ||
| 239 | # pinning. | ||
| 240 | for _hs in "$SOCKH1" "$SOCKH2"; do | ||
| 241 | if "$MUXD" stats --sock "$_hs" | grep -q 'session 1 '; then | ||
| 242 | pipe_mux "$OUT.hkill" "$OUT.hkill.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 \ | ||
| 243 | "$MUX" --sock "$_hs" --session 1 | ||
| 244 | pipe_send 'exit 0\n' | ||
| 245 | pipe_waitexit "hosts: the born session" | ||
| 246 | fi | ||
| 247 | done | ||
| 248 | wait_sessions "$SOCKH1" 2 "hosts: daemon 1 back to 0 and b" | ||
| 249 | wait_sessions "$SOCKH2" 2 "hosts: daemon 2 back to 0 and c" | ||
| 250 | ok "every live session of every listed daemon is a tile, n walks them, and a birth writes nothing down" | ||
| 251 | |||
| 252 | # ---- Ctrl-\ x ends a session, and asks first ---------------------------- | ||
| 253 | # | ||
| 254 | # `x` is the one chord that destroys something, so it is the one chord that | ||
| 255 | # refuses. Two clients on one session, and the three facts that make the | ||
| 256 | # two-step a two-step rather than a delay: | ||
| 257 | # | ||
| 258 | # * the first `x` is REFUSED, by count, and the shell is still there — | ||
| 259 | # asked of the OS with `kill -0` on the pid the shell itself printed, | ||
| 260 | # not of the daemon that would be reporting on its own refusal. | ||
| 261 | # * the second `x` ends it. The shell is gone from the process table. | ||
| 262 | # * the OTHER client, which typed nothing at all, comes back with the | ||
| 263 | # session's exit status. A hangup is not a detach: it reaches everyone | ||
| 264 | # attached. | ||
| 265 | # | ||
| 266 | # Two ptyclient runs rather than one, because `kill -0` between the two | ||
| 267 | # chords is this script's to do and ptyclient's script cannot stop for it. | ||
| 268 | # The three-second latch means run B has to press `x` twice itself. | ||
| 269 | pipe_mux "$OUT.hxmk" "$OUT.hxmk.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 \ | ||
| 270 | "$MUX" --sock "$SOCKH1" --session xb | ||
| 271 | pipe_send 'echo xbpid=$$\n' | ||
| 272 | await_out "$OUT.hxmk" "xbpid=" "xbpid never reached the client" | ||
| 273 | pipe_detach "hosts x setup" | ||
| 274 | wait_grid "$SOCKH1" "xbpid=" "hosts x: session xb's shell pid" xb | ||
| 275 | XBPID=$(dump_session "$SOCKH1" xb | sed -n 's/.*xbpid=\([0-9]*\).*/\1/p' | tail -1) | ||
| 276 | [ -n "$XBPID" ] || { echo "e2e FAIL: hosts x: session xb printed no pid"; exit 1; } | ||
| 277 | |||
| 278 | # The other client: attached, silent, and holding its stdin open so nothing | ||
| 279 | # but the session's own end can bring it back. | ||
| 280 | pipe_mux "$OUT.hxo" "$OUT.hxo.err" env XDG_STATE_HOME="$HXSTATE" timeout 90 \ | ||
| 281 | "$MUX" --sock "$SOCKH1" --session xb | ||
| 282 | await_out "$OUT.hxo" "xbpid=" "hosts x: the other client never got the grid" | ||
| 283 | |||
| 284 | set +e | ||
| 285 | XDG_STATE_HOME="$HXSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 30 \ | ||
| 286 | --out "$OUT.hx1cap" --err "$OUT.hx1cap.err" -- \ | ||
| 287 | "$MUX" --sock "$SOCKH1" --session xb > "$OUT.hx1pc" 2>&1 <<'EOF' | ||
| 288 | expect xbpid= 20000 | ||
| 289 | settle 800 20000 | ||
| 290 | send \x1cx | ||
| 291 | expect attached - x again to end 15000 | ||
| 292 | settle 600 15000 | ||
| 293 | send \x1cd | ||
| 294 | waitexit 10000 | ||
| 295 | EOF | ||
| 296 | RC=$? | ||
| 297 | set -e | ||
| 298 | [ "$RC" -eq 0 ] || { | ||
| 299 | echo "e2e FAIL: hosts x: the refusal leg exited $RC (did the first x say who else was there?):" | ||
| 300 | cat "$OUT.hx1pc"; exit 1; } | ||
| 301 | grep -qF -- "[1 other attached - x again to end]" "$OUT.hx1cap" || { | ||
| 302 | echo "e2e FAIL: hosts x: the refusal did not name the count:" | ||
| 303 | grep -ao '\[[0-9]* other[^]]*\]' "$OUT.hx1cap" | head -3; exit 1; } | ||
| 304 | kill -0 "$XBPID" 2>/dev/null || { | ||
| 305 | echo "e2e FAIL: hosts x: the FIRST x ended the shell despite another client"; exit 1; } | ||
| 306 | |||
| 307 | set +e | ||
| 308 | XDG_STATE_HOME="$HXSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 30 \ | ||
| 309 | --out "$OUT.hx2cap" --err "$OUT.hx2cap.err" -- \ | ||
| 310 | "$MUX" --sock "$SOCKH1" --session xb > "$OUT.hx2pc" 2>&1 <<'EOF' | ||
| 311 | expect xbpid= 20000 | ||
| 312 | settle 800 20000 | ||
| 313 | send \x1cx | ||
| 314 | expect attached - x again to end 15000 | ||
| 315 | settle 600 15000 | ||
| 316 | send \x1cx | ||
| 317 | settle 2000 20000 | ||
| 318 | send \x1cd | ||
| 319 | waitexit 10000 | ||
| 320 | EOF | ||
| 321 | RC=$? | ||
| 322 | set -e | ||
| 323 | [ "$RC" -eq 0 ] || { | ||
| 324 | echo "e2e FAIL: hosts x: the forcing leg exited $RC:"; cat "$OUT.hx2pc"; exit 1; } | ||
| 325 | wait_pid_gone "$XBPID" "hosts x: the shell after the second x" | ||
| 326 | # The other client was hung up on, with the session's status in hand. 128 | ||
| 327 | # is what a shell killed by a signal reports (pty.zig checkExited), and it | ||
| 328 | # is the one number that says the daemon ended the session rather than the | ||
| 329 | # client noticing a closed socket. | ||
| 330 | pipe_waitexit "hosts x: the other client" 128 | ||
| 331 | wait_sessions "$SOCKH1" 2 "hosts x: only xb ended" | ||
| 332 | ok "x refuses while others are attached, then ends; the other client sees the exit" | ||
| 333 | |||
| 334 | # ---- a daemon that goes and comes back re-creates NOTHING --------------- | ||
| 335 | # | ||
| 336 | # The model's sharpest edge. Sessions live in the daemon, so a daemon that | ||
| 337 | # dies takes them with it — and the wall, which is a list of DAEMONS, has | ||
| 338 | # nothing left to resurrect them from. The old wall file could and did. | ||
| 339 | # | ||
| 340 | # Two states of one host, each with its own witness: | ||
| 341 | # | ||
| 342 | # * down: ONE stripe, `[unreachable]`, and not one dead tile per session | ||
| 343 | # it used to have. | ||
| 344 | # * back: its own default session and nothing else. `c`, which was live | ||
| 345 | # when the daemon died, is not there — asked of `muxd stats` on the | ||
| 346 | # real daemon, not of the wall that would be reporting on its own | ||
| 347 | # resurrection. | ||
| 348 | assert_stopped "$SOCKH2" "$DH2PID" "hosts restart" "$OUT.h2stop" | ||
| 349 | DH2PID="" | ||
| 350 | set +e | ||
| 351 | XDG_STATE_HOME="$HSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 44 \ | ||
| 352 | --out "$OUT.hdcap" --err "$OUT.hdcap.err" -- "$MUX" > "$OUT.hdpc" 2>&1 <<'EOF' | ||
| 353 | expect [unreachable] 25000 | ||
| 354 | settle 1000 25000 | ||
| 355 | send \x1cd | ||
| 356 | waitexit 15000 | ||
| 357 | EOF | ||
| 358 | RC=$? | ||
| 359 | set -e | ||
| 360 | [ "$RC" -eq 0 ] || { | ||
| 361 | echo "e2e FAIL: hosts restart: the down-host wall exited $RC:"; cat "$OUT.hdpc"; exit 1; } | ||
| 362 | grep -q -- "--sock $SOCKH2 \[unreachable\]" "$OUT.hdcap" || { | ||
| 363 | echo "e2e FAIL: hosts restart: a down daemon is not one stripe naming itself:" | ||
| 364 | cat "$OUT.hdpc"; exit 1; } | ||
| 365 | if grep -q -- "--sock $SOCKH2#c" "$OUT.hdcap"; then | ||
| 366 | echo "e2e FAIL: hosts restart: a down daemon still shows the sessions it had:" | ||
| 367 | cat "$OUT.hdpc"; exit 1 | ||
| 368 | fi | ||
| 369 | |||
| 370 | start_daemon "$SOCKH2" "$OUT.h2b.d" "hosts daemon 2 never came back" --shell /bin/sh | ||
| 371 | DH2PID=$DPID | ||
| 372 | set +e | ||
| 373 | XDG_STATE_HOME="$HSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 44 \ | ||
| 374 | --out "$OUT.hrcap" --err "$OUT.hrcap.err" -- "$MUX" > "$OUT.hrpc" 2>&1 <<'EOF' | ||
| 375 | expect [up] 25000 | ||
| 376 | settle 2500 25000 | ||
| 377 | send \x1cd | ||
| 378 | waitexit 15000 | ||
| 379 | EOF | ||
| 380 | RC=$? | ||
| 381 | set -e | ||
| 382 | [ "$RC" -eq 0 ] || { | ||
| 383 | echo "e2e FAIL: hosts restart: the reborn-host wall exited $RC:"; cat "$OUT.hrpc"; exit 1; } | ||
| 384 | grep -q -- "--sock $SOCKH2#0 \[up\]" "$OUT.hrcap" || { | ||
| 385 | echo "e2e FAIL: hosts restart: the reborn daemon's own session never became a tile:" | ||
| 386 | cat "$OUT.hrpc"; exit 1; } | ||
| 387 | if grep -q -- "--sock $SOCKH2#c" "$OUT.hrcap"; then | ||
| 388 | echo "e2e FAIL: hosts restart: the wall resurrected the session the dead daemon held:" | ||
| 389 | cat "$OUT.hrpc"; exit 1 | ||
| 390 | fi | ||
| 391 | # The daemon's own account of it, which is the one that matters: a session | ||
| 392 | # the wall re-created would be here whether or not it ever got a stripe. | ||
| 393 | [ "$("$MUXD" stats --sock "$SOCKH2" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p')" = "1" ] || { | ||
| 394 | echo "e2e FAIL: hosts restart: the reborn daemon holds more than its own default session:" | ||
| 395 | "$MUXD" stats --sock "$SOCKH2"; exit 1; } | ||
| 396 | ok "a daemon that goes down is one unreachable stripe, and comes back re-creating nothing" | ||
| 397 | |||
| 398 | # ---- hosts rm takes a daemon off the wall and ends nothing -------------- | ||
| 399 | # | ||
| 400 | # The file is a VIEW. Removing a line is forgetting where to look, never | ||
| 401 | # an instruction to the daemon — which is why the sessions are asked for | ||
| 402 | # again afterwards and have to still be there. | ||
| 403 | # | ||
| 404 | # The two refusals ride along because they are the same file's rules: | ||
| 405 | # `rm` of a line that is not there is an error naming the file, and `add` | ||
| 406 | # of a spelling with a `#` in it is the grammar's one refusal. | ||
| 407 | # `timeout`, and a verdict of its own: `mux hosts` dials every listed | ||
| 408 | # daemon, and while each dial is bounded inside the binary (hosts_list_ms) a | ||
| 409 | # hang here would hang the whole suite with no line naming it. Bare under | ||
| 410 | # `set -e` a non-zero exit would abort the run with no message at all. | ||
| 411 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts > "$OUT.hlist" 2>&1 || { | ||
| 412 | echo "e2e FAIL: hosts rm: 'mux hosts' exited nonzero on two live daemons:" | ||
| 413 | cat "$OUT.hlist"; exit 1; } | ||
| 414 | grep -qF -- "--sock $SOCKH1" "$OUT.hlist" || { | ||
| 415 | echo "e2e FAIL: hosts rm: 'mux hosts' does not list the daemon it is about to remove:" | ||
| 416 | cat "$OUT.hlist"; exit 1; } | ||
| 417 | # The COUNT, which is the whole point of the verb — and the two daemons hold | ||
| 418 | # different numbers on purpose: a fixture where every row carried the same | ||
| 419 | # figure could not tell the daemon's own answer from a constant, and one | ||
| 420 | # where every row failed (the only case pinned before) never ran the column | ||
| 421 | # at all. `wait_sessions` above already settled both. | ||
| 422 | for _hc in "$SOCKH1:2" "$SOCKH2:1"; do | ||
| 423 | _hcs=${_hc%:*}; _hcn=${_hc##*:} | ||
| 424 | grep -qxF -- "$(printf -- '--sock %s\t%s' "$_hcs" "$_hcn")" "$OUT.hlist" || { | ||
| 425 | echo "e2e FAIL: hosts: 'mux hosts' does not say $_hcs holds $_hcn sessions:" | ||
| 426 | cat "$OUT.hlist"; exit 1; } | ||
| 427 | done | ||
| 428 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts rm "--sock $SOCKH1" > "$OUT.hrm" 2>&1 || { | ||
| 429 | echo "e2e FAIL: hosts rm: removing a listed daemon failed:"; cat "$OUT.hrm"; exit 1; } | ||
| 430 | if grep -qF -- "$SOCKH1" "$HSTATE/mux/hosts"; then | ||
| 431 | echo "e2e FAIL: hosts rm: the line is still on the wall:" | ||
| 432 | cat "$HSTATE/mux/hosts"; exit 1 | ||
| 433 | fi | ||
| 434 | wait_sessions "$SOCKH1" 2 "hosts rm: daemon 1's sessions must survive being forgotten" | ||
| 435 | wait_grid "$SOCKH1" "hb-pin" "hosts rm: session b's grid outlived the wall line" b | ||
| 436 | |||
| 437 | set +e | ||
| 438 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts rm "--sock $SOCKH1" > "$OUT.hrm2" 2>&1 | ||
| 439 | RC=$? | ||
| 440 | set -e | ||
| 441 | [ "$RC" -eq 1 ] || { | ||
| 442 | echo "e2e FAIL: hosts rm: removing an absent daemon exited $RC, want 1:" | ||
| 443 | cat "$OUT.hrm2"; exit 1; } | ||
| 444 | grep -q "not on the wall" "$OUT.hrm2" || { | ||
| 445 | echo "e2e FAIL: hosts rm: an absent rm never said so:"; cat "$OUT.hrm2"; exit 1; } | ||
| 446 | set +e | ||
| 447 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts add "--sock $SOCKH1#b" > "$OUT.hadd2" 2>&1 | ||
| 448 | RC=$? | ||
| 449 | set -e | ||
| 450 | [ "$RC" -eq 2 ] || { | ||
| 451 | echo "e2e FAIL: hosts add: a '#' spelling exited $RC, want 2:"; cat "$OUT.hadd2"; exit 1; } | ||
| 452 | grep -q "names a session" "$OUT.hadd2" || { | ||
| 453 | echo "e2e FAIL: hosts add: the '#' refusal never named the rule:"; cat "$OUT.hadd2"; exit 1; } | ||
| 454 | if grep -qF -- "$SOCKH1" "$HSTATE/mux/hosts"; then | ||
| 455 | echo "e2e FAIL: hosts add: a refused spelling still edited the file:" | ||
| 456 | cat "$HSTATE/mux/hosts"; exit 1 | ||
| 457 | fi | ||
| 458 | # Asking what a command does is not mistyping its verb. `--help` and | ||
| 459 | # `--version` used to be answered inside the `add` branch only, so `mux | ||
| 460 | # hosts --help` exited 2 calling the flag a subcommand that does not exist, | ||
| 461 | # and sent the reader looking for one. Asserted here rather than in a unit | ||
| 462 | # test because the answer is a page on fd 1, which under `zig build test` is | ||
| 463 | # the runner's own protocol stream. | ||
| 464 | for _hh in "--help" "-h"; do | ||
| 465 | set +e | ||
| 466 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts "$_hh" > "$OUT.hhelp" 2>&1 | ||
| 467 | RC=$? | ||
| 468 | set -e | ||
| 469 | [ "$RC" -eq 0 ] || { | ||
| 470 | echo "e2e FAIL: hosts $_hh: exited $RC, want 0:"; cat "$OUT.hhelp"; exit 1; } | ||
| 471 | grep -q "mux hosts add SPELLING" "$OUT.hhelp" || { | ||
| 472 | echo "e2e FAIL: hosts $_hh: answered something other than the usage:" | ||
| 473 | cat "$OUT.hhelp"; exit 1; } | ||
| 474 | done | ||
| 475 | set +e | ||
| 476 | env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts --version > "$OUT.hver" 2>&1 | ||
| 477 | RC=$? | ||
| 478 | set -e | ||
| 479 | [ "$RC" -eq 0 ] || { | ||
| 480 | echo "e2e FAIL: hosts --version: exited $RC, want 0:"; cat "$OUT.hver"; exit 1; } | ||
| 481 | grep -q "^mux " "$OUT.hver" || { | ||
| 482 | echo "e2e FAIL: hosts --version: did not name the binary and its version:" | ||
| 483 | cat "$OUT.hver"; exit 1; } | ||
| 484 | |||
| 485 | assert_stopped "$SOCKH1" "$DH1PID" "hosts rm" "$OUT.h1stop" | ||
| 486 | DH1PID="" | ||
| 487 | assert_stopped "$SOCKH2" "$DH2PID" "hosts rm" "$OUT.h2stop2" | ||
| 488 | DH2PID="" | ||
| 489 | ok "hosts rm takes the daemon off the wall and ends nothing" | ||
test/e2e_09_wallhist.sh
| Old | New | ||
|---|---|---|---|
| @@ -1,636 +0,0 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_09_wallhist.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in | ||
| 3 | # the order they stand in; see the lib's header for what this file may | ||
| 4 | # assume and what it must register. | ||
| 5 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its | ||
| 6 | # own, for the dynamic-wall leg's reason turned up one notch: what these | ||
| 7 | # two blocks read back is the wall FILE, and every `mux` in this suite now | ||
| 8 | # writes a tile into the shared $XDG_STATE_HOME — so a file read out of | ||
| 9 | # that one would be every other block's attaches as much as this one's. | ||
| 10 | # The daemon is separate because the tile spelling contains the socket | ||
| 11 | # path, which is what the assertions grep for. | ||
| 12 | SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock" | ||
| 13 | defer_sock "$SOCK40" | ||
| 14 | WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$" | ||
| 15 | defer_rm "$WHSTATE" | ||
| 16 | WHWALL="$WHSTATE/mux/wall" | ||
| 17 | # A state home whose wall FILE is a directory: the unwritable case, which | ||
| 18 | # has to warn and let the attach happen anyway. | ||
| 19 | WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$" | ||
| 20 | defer_rm "$WHBAD" | ||
| 21 | # And a third, for the `x` block: its wall is built by two attaches and | ||
| 22 | # then eaten by `x`, so it must start empty and stay its own. | ||
| 23 | WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$" | ||
| 24 | defer_rm "$WHXSTATE" | ||
| 25 | # The convergence block: `mux TARGET` is a wall of one tile whose rect is | ||
| 26 | # the whole terminal. Three daemons and three state homes, each of them its own | ||
| 27 | # for the same reason the attach-history block's is — what these legs read | ||
| 28 | # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the | ||
| 29 | # suite has ever attached to. | ||
| 30 | SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock" | ||
| 31 | defer_sock "$SOCK45" | ||
| 32 | CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$" | ||
| 33 | defer_rm "$CVSTATE" | ||
| 34 | CVWALL="$CVSTATE/mux/wall" | ||
| 35 | # The ring-grows-the-wall leg needs TWO state homes: one for the client | ||
| 36 | # under test, and one for the setup client whose attach must NOT leave a | ||
| 37 | # tile the leg would then find already there. That absence is the case | ||
| 38 | # under test — a sibling the daemon knows and the wall does not. | ||
| 39 | SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock" | ||
| 40 | defer_sock "$SOCK46" | ||
| 41 | RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$" | ||
| 42 | defer_rm "$RGSTATE" | ||
| 43 | RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$" | ||
| 44 | defer_rm "$RGOTHER" | ||
| 45 | # The exit-semantics leg: a session ending in the focused tile. | ||
| 46 | SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" | ||
| 47 | defer_sock "$SOCK47" | ||
| 48 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" | ||
| 49 | defer_rm "$XESTATE" | ||
| 50 | |||
| 51 | # ---- the wall is attach HISTORY ----------------------------------------- | ||
| 52 | # | ||
| 53 | # The mechanical rule (wall-home-screen spec, phase 2): an attach that | ||
| 54 | # CLAIMS the grid writes its tile; one that does not, does not. Four | ||
| 55 | # witnesses, in one state home nothing else writes to: | ||
| 56 | # | ||
| 57 | # * a `mux` attach adds exactly one line, spelled `#0` — the RESOLVED | ||
| 58 | # default name, because a wall line has to be a spelling a user could | ||
| 59 | # type back, not the empty wire name the attach frame carries; | ||
| 60 | # * a second attach to the same session adds nothing (dedup, byte-exact); | ||
| 61 | # * `muxa` drives the SAME session and the file is byte-identical after | ||
| 62 | # — the 0x0 invariant, made testable rather than merely stated. The | ||
| 63 | # hash is anchored on a marker the agent actually landed, so a muxa | ||
| 64 | # that did nothing at all cannot pass this leg by doing nothing; | ||
| 65 | # * a session reached by the `Ctrl-\ c` chord earns its own tile, which | ||
| 66 | # is the "the wall grows by the truth" clause — those re-dials happen | ||
| 67 | # inside client.attach's own loop and never come back through main. | ||
| 68 | # | ||
| 69 | # Then the two failure faces: an unwritable wall file warns and does NOT | ||
| 70 | # stop the attach, and 'mux wall add' refuses a bad spelling by name | ||
| 71 | # without touching the file. | ||
| 72 | start_daemon "$SOCK40" "$OUT.wh.d" "attach-history daemon never bound" --shell /bin/sh | ||
| 73 | D37PID=$DPID | ||
| 74 | |||
| 75 | pipe_mux "$OUT.wh1" "$OUT.wh1.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 76 | pipe_send 'printf "wh1-%%s\\n" pin\n' | ||
| 77 | await_out "$OUT.wh1" "wh1-pin" "wh1-pin never reached the client" | ||
| 78 | pipe_detach | ||
| 79 | wait_grid "$SOCK40" "wh1-pin" "attach history: the default session's marker" | ||
| 80 | grep -qx -- "--sock $SOCK40#0" "$WHWALL" || { | ||
| 81 | echo "e2e FAIL: attach history: the attach wrote no tile (or not '#0'):" | ||
| 82 | cat "$WHWALL" 2>&1; exit 1; } | ||
| 83 | _wh_n=$(wc -l < "$WHWALL") | ||
| 84 | [ "$_wh_n" -eq 1 ] || { | ||
| 85 | echo "e2e FAIL: attach history: one attach wrote $_wh_n lines:" | ||
| 86 | cat "$WHWALL"; exit 1; } | ||
| 87 | |||
| 88 | pipe_mux "$OUT.wh2" "$OUT.wh2.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 89 | pipe_send 'printf "wh2-%%s\\n" pin\n' | ||
| 90 | await_out "$OUT.wh2" "wh2-pin" "wh2-pin never reached the client" | ||
| 91 | pipe_detach | ||
| 92 | wait_grid "$SOCK40" "wh2-pin" "attach history: the second attach's marker" | ||
| 93 | _wh_n=$(wc -l < "$WHWALL") | ||
| 94 | [ "$_wh_n" -eq 1 ] || { | ||
| 95 | echo "e2e FAIL: attach history: a second attach to the same session made" | ||
| 96 | echo " $_wh_n lines — dedup is not deduping:" | ||
| 97 | cat "$WHWALL"; exit 1; } | ||
| 98 | |||
| 99 | # muxa: the 0x0 invariant, as a hash. Its marker is the anchor — a muxa | ||
| 100 | # that never attached would leave the file identical too, and this leg | ||
| 101 | # would pass while proving nothing. | ||
| 102 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 103 | XDG_STATE_HOME="$WHSTATE" "$MUXA" send 'printf "whagent-%s\n" pin\n' \ | ||
| 104 | --sock "$SOCK40" > "$OUT.whb" 2> "$OUT.whb.err" | ||
| 105 | wait_grid "$SOCK40" "whagent-pin" "attach history: the agent's own marker" | ||
| 106 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 107 | echo "e2e FAIL: attach history: muxa attached at 0x0 and still wrote a tile" | ||
| 108 | echo " — 'muxa never touches the wall' is broken:" | ||
| 109 | cat "$WHWALL"; exit 1; } | ||
| 110 | |||
| 111 | # Ctrl-\ c: the chord re-dials full size, so the session it creates earns a | ||
| 112 | # tile like any other attach. The chord gets a write of its own — a chord | ||
| 113 | # ends its chunk, and a piped client reads chunks. | ||
| 114 | { sleep 1; printf '\034c'; sleep 3; printf 'printf "whc-%%s\\n" pin\n'; \ | ||
| 115 | sleep 2; printf '\034\034'; } | \ | ||
| 116 | XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" \ | ||
| 117 | > "$OUT.whc" 2> "$OUT.whc.err" | ||
| 118 | wait_grid "$SOCK40" "whc-pin" "attach history: the chord-created session's marker" 1 | ||
| 119 | grep -qx -- "--sock $SOCK40#1" "$WHWALL" || { | ||
| 120 | echo "e2e FAIL: attach history: a session visited by Ctrl-\\ c earned no tile:" | ||
| 121 | cat "$WHWALL"; cat "$OUT.whc.err"; exit 1; } | ||
| 122 | |||
| 123 | # An unwritable wall file: a DIRECTORY where the file belongs. One warning, | ||
| 124 | # and an attach that happened anyway — best effort means the attach is the | ||
| 125 | # act and the tile is only the record. | ||
| 126 | mkdir -p "$WHBAD/mux/wall" | ||
| 127 | pipe_mux "$OUT.whro" "$OUT.whro.err" env XDG_STATE_HOME="$WHBAD" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 128 | pipe_send 'printf "whro-%%s\\n" pin\n' | ||
| 129 | await_out "$OUT.whro" "whro-pin" "whro-pin never reached the client" | ||
| 130 | pipe_detach | ||
| 131 | grep -q "wall not updated" "$OUT.whro.err" || { | ||
| 132 | echo "e2e FAIL: attach history: an unwritable wall file said nothing:" | ||
| 133 | cat "$OUT.whro.err"; exit 1; } | ||
| 134 | wait_grid "$SOCK40" "whro-pin" "attach history: the attach an unwritable wall did not block" | ||
| 135 | |||
| 136 | # The self-attach refusal exits BEFORE the dial, so it must exit before the | ||
| 137 | # write too: a tile is the record of an attach, and this one never happened. | ||
| 138 | # Spelled with the environment rather than a session shell because that is | ||
| 139 | # all the refusal reads — and it keeps the assertion a hash of one file | ||
| 140 | # rather than a search for a line other legs also write. | ||
| 141 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 142 | set +e | ||
| 143 | MUX_SOCK="$SOCK40" MUX_SESSION=0 XDG_STATE_HOME="$WHSTATE" \ | ||
| 144 | "$MUX" --sock "$SOCK40" > "$OUT.whself" 2>&1 | ||
| 145 | RC=$? | ||
| 146 | set -e | ||
| 147 | [ "$RC" -eq 2 ] || { | ||
| 148 | echo "e2e FAIL: attach history: the self-attach refusal exited $RC, want 2:" | ||
| 149 | cat "$OUT.whself"; exit 1; } | ||
| 150 | grep -q "this shell is inside that session" "$OUT.whself" || { | ||
| 151 | echo "e2e FAIL: attach history: the self-attach leg refused for another reason:" | ||
| 152 | cat "$OUT.whself"; exit 1; } | ||
| 153 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 154 | echo "e2e FAIL: attach history: a REFUSED self-attach still wrote a tile:" | ||
| 155 | cat "$WHWALL"; exit 1; } | ||
| 156 | |||
| 157 | # 'mux wall add': validation at ADD time, in the grammar's own words, and | ||
| 158 | # a refused spelling writes nothing at all. | ||
| 159 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 160 | set +e | ||
| 161 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall add "box#has space" > "$OUT.whadd" 2>&1 | ||
| 162 | RC=$? | ||
| 163 | set -e | ||
| 164 | [ "$RC" -ne 0 ] || { | ||
| 165 | echo "e2e FAIL: attach history: 'mux wall add' accepted a bad session name"; exit 1; } | ||
| 166 | grep -q "bad session name" "$OUT.whadd" || { | ||
| 167 | echo "e2e FAIL: attach history: a refused add never named its reason:" | ||
| 168 | cat "$OUT.whadd"; exit 1; } | ||
| 169 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 170 | echo "e2e FAIL: attach history: a refused add still edited the file:" | ||
| 171 | cat "$WHWALL"; exit 1; } | ||
| 172 | |||
| 173 | # ...and the pair that works, including `rm` of what is not there. | ||
| 174 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall add "whbox#work" > "$OUT.whadd" 2>&1 | ||
| 175 | grep -qx "whbox#work" "$WHWALL" || { | ||
| 176 | echo "e2e FAIL: attach history: 'mux wall add' added nothing:"; cat "$WHWALL"; exit 1; } | ||
| 177 | set +e | ||
| 178 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall rm "whbox#work" > "$OUT.whrm" 2>&1 | ||
| 179 | RC=$? | ||
| 180 | set -e | ||
| 181 | [ "$RC" -eq 0 ] || { | ||
| 182 | echo "e2e FAIL: attach history: 'mux wall rm' exited $RC:"; cat "$OUT.whrm"; exit 1; } | ||
| 183 | if grep -q "whbox#work" "$WHWALL"; then | ||
| 184 | echo "e2e FAIL: attach history: 'mux wall rm' left the line behind:" | ||
| 185 | cat "$WHWALL"; exit 1 | ||
| 186 | fi | ||
| 187 | set +e | ||
| 188 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall rm "whbox#work" > "$OUT.whrm" 2>&1 | ||
| 189 | RC=$? | ||
| 190 | set -e | ||
| 191 | [ "$RC" -ne 0 ] || { | ||
| 192 | echo "e2e FAIL: attach history: removing an absent tile exited 0"; exit 1; } | ||
| 193 | grep -q "not on the wall" "$OUT.whrm" || { | ||
| 194 | echo "e2e FAIL: attach history: an absent rm never said so:"; cat "$OUT.whrm"; exit 1; } | ||
| 195 | ok "the wall is attach history: mux adds, muxa never does, add/rm edit it" | ||
| 196 | |||
| 197 | # ---- `x` forgets a tile, and never kills its session -------------------- | ||
| 198 | # | ||
| 199 | # The wall this leg puts up is one nothing typed by hand: two attaches | ||
| 200 | # BUILT it, which is the model's whole claim. Then `Ctrl-\ x` is asked for | ||
| 201 | # the two things it must do and the one it must not — remove the line, | ||
| 202 | # re-cut the wall, and leave the session running ("remove is detach"). | ||
| 203 | # | ||
| 204 | # Two ptyclient runs rather than one, because the file is the artifact: | ||
| 205 | # what a run did is only readable after it has exited, so the one-tile | ||
| 206 | # state between them is the second run's input. | ||
| 207 | pipe_mux "$OUT.whxa" "$OUT.whxa.err" env XDG_STATE_HOME="$WHXSTATE" timeout 40 "$MUX" --sock "$SOCK40" --session xa | ||
| 208 | pipe_send 'printf "whxa-%%s\\n" pin\n' | ||
| 209 | await_out "$OUT.whxa" "whxa-pin" "whxa-pin never reached the client" | ||
| 210 | pipe_detach | ||
| 211 | wait_grid "$SOCK40" "whxa-pin" "x forgets: session xa's marker" xa | ||
| 212 | pipe_mux "$OUT.whxb" "$OUT.whxb.err" env XDG_STATE_HOME="$WHXSTATE" timeout 40 "$MUX" --sock "$SOCK40" --session xb | ||
| 213 | pipe_send 'printf "whxb-%%s\\n" pin\n' | ||
| 214 | await_out "$OUT.whxb" "whxb-pin" "whxb-pin never reached the client" | ||
| 215 | pipe_detach | ||
| 216 | wait_grid "$SOCK40" "whxb-pin" "x forgets: session xb's marker" xb | ||
| 217 | _whx_n=$(wc -l < "$WHXSTATE/mux/wall") | ||
| 218 | [ "$_whx_n" -eq 2 ] || { | ||
| 219 | echo "e2e FAIL: x forgets: two attaches built a wall of $_whx_n tiles:" | ||
| 220 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 221 | |||
| 222 | # No operands: the wall this shows is the file, which is the point. The | ||
| 223 | # state home rides on PTYCLIENT itself and is inherited by the child — | ||
| 224 | # ptyclient execs its `--` argv as given, so an `env` wrapper in front of | ||
| 225 | # `mux` is not a command it runs, it is a program it fails to find. | ||
| 226 | # | ||
| 227 | # ONE marker expected, then `settle` — the pattern every other two-tile | ||
| 228 | # ptyclient leg here uses, and for a reason review reproduced on this | ||
| 229 | # suite: the tiles are independent threads, `expect` consumes forward, and | ||
| 230 | # whichever stripe painted second leaves the other one's marker BEHIND the | ||
| 231 | # cursor. Two consecutive expects against two pumps is a coin flip that | ||
| 232 | # spends its whole budget waiting for bytes that already went past. What | ||
| 233 | # the `Ctrl-\ x` below needs is a wall that is up and settled, which the | ||
| 234 | # single expect plus `settle` gives; the tile it forgets is the FOCUSED | ||
| 235 | # one, tile 0, whose line the file is then asserted NOT to hold. | ||
| 236 | set +e | ||
| 237 | XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 238 | --out "$OUT.whxcap" --err "$OUT.whxcap.err" -- \ | ||
| 239 | "$MUX" wall > "$OUT.whxpc" 2>&1 <<'EOF' | ||
| 240 | expect whxb-pin 20000 | ||
| 241 | settle 800 20000 | ||
| 242 | send \x1cx | ||
| 243 | settle 800 20000 | ||
| 244 | send \x1cd | ||
| 245 | waitexit 10000 | ||
| 246 | EOF | ||
| 247 | RC=$? | ||
| 248 | set -e | ||
| 249 | [ "$RC" -eq 0 ] || { | ||
| 250 | echo "e2e FAIL: x forgets: ptyclient leg exited $RC (did \\x1cx forget the tile?):" | ||
| 251 | cat "$OUT.whxpc"; exit 1; } | ||
| 252 | # The line is gone and the survivor kept its place — `Ctrl-\ x` is an | ||
| 253 | # ordered removal, not a rewrite of the wall. | ||
| 254 | _whx_n=$(wc -l < "$WHXSTATE/mux/wall") | ||
| 255 | [ "$_whx_n" -eq 1 ] || { | ||
| 256 | echo "e2e FAIL: x forgets: the wall file holds $_whx_n lines after one \\x1cx:" | ||
| 257 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 258 | grep -qx -- "--sock $SOCK40#xb" "$WHXSTATE/mux/wall" || { | ||
| 259 | echo "e2e FAIL: x forgets: the wrong line survived:" | ||
| 260 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 261 | # The survivor took the whole terminal: its marker painted a SECOND time | ||
| 262 | # after the re-cut, counted because one paint is the wall's first draw and | ||
| 263 | # says nothing about the hole closing around the survivor. | ||
| 264 | _whx_bars=$(grep -o "whxb-pin" "$OUT.whxcap" | wc -l) | ||
| 265 | [ "$_whx_bars" -ge 2 ] || { | ||
| 266 | echo "e2e FAIL: x forgets: the surviving tile's marker painted $_whx_bars time(s)," | ||
| 267 | echo " so the wall was never re-cut around the hole:" | ||
| 268 | cat "$OUT.whxpc"; exit 1; } | ||
| 269 | # ...and the SESSION is untouched: it still answers, and it still holds | ||
| 270 | # what it held. "Remove is detach" — the tile went, the session did not. | ||
| 271 | # xa was 80-wide from its pipe attach; this 40-wide wall resized it on | ||
| 272 | # hydrate, and the detach left that size behind. | ||
| 273 | XDG_STATE_HOME="$WHSTATE" "$MUXA" status --sock "$SOCK40" --session xa > "$OUT.whxst" 2>&1 | ||
| 274 | grep -q '"cols":40' "$OUT.whxst" || { | ||
| 275 | echo "e2e FAIL: x forgets: session xa stopped answering — \\x1cx killed it:" | ||
| 276 | cat "$OUT.whxst"; exit 1; } | ||
| 277 | wait_grid "$SOCK40" "whxa-pin" "x forgets: xa's grid outlived its tile" xa | ||
| 278 | |||
| 279 | # The last tile: an empty wall SAYS so rather than going blank. | ||
| 280 | set +e | ||
| 281 | XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 282 | --out "$OUT.whxcap2" --err "$OUT.whxcap2.err" -- \ | ||
| 283 | "$MUX" wall > "$OUT.whxpc2" 2>&1 <<'EOF' | ||
| 284 | expect whxb-pin 20000 | ||
| 285 | settle 800 20000 | ||
| 286 | send \x1cx | ||
| 287 | settle 800 20000 | ||
| 288 | send \x1cd | ||
| 289 | waitexit 10000 | ||
| 290 | EOF | ||
| 291 | RC=$? | ||
| 292 | set -e | ||
| 293 | [ "$RC" -eq 0 ] || { | ||
| 294 | echo "e2e FAIL: x forgets: the last-tile leg exited $RC:" | ||
| 295 | cat "$OUT.whxpc2"; exit 1; } | ||
| 296 | grep -q "nothing left to show" "$OUT.whxcap2" || { | ||
| 297 | echo "e2e FAIL: x forgets: forgetting the last tile left a blank terminal" | ||
| 298 | echo " with nothing said:"; cat "$OUT.whxpc2"; exit 1; } | ||
| 299 | [ ! -s "$WHXSTATE/mux/wall" ] || { | ||
| 300 | echo "e2e FAIL: x forgets: the last \\x1cx left lines behind:" | ||
| 301 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 302 | # The phantom tile, while this daemon is FULL: this leg's own four (0, 1, | ||
| 303 | # xa, xb) plus the 28 filled here are max_sessions, so a FIRST attach to | ||
| 304 | # one more name is refused — and a refusal must leave no line behind. Its | ||
| 305 | # state home is neither $WHSTATE nor $WHXSTATE, both of whose walls are | ||
| 306 | # asserted on. This is the case that made the seam move off "the dial | ||
| 307 | # succeeded" — a dial that comes up is not an attach that landed, and only | ||
| 308 | # a SWITCH's refusal ever had somewhere to undo the write from. The leg | ||
| 309 | # cannot pass vacuously: a daemon with room would accept the attach and | ||
| 310 | # the rc check below would fail loudly. | ||
| 311 | fill_sessions "$SOCK40" "$OUT.whfill" fill 4 31 | ||
| 312 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 313 | set +e | ||
| 314 | { sleep 1; printf '\034\034'; } | XDG_STATE_HOME="$WHSTATE" timeout 40 \ | ||
| 315 | "$MUX" --sock "$SOCK40" --session phantom > "$OUT.whph" 2>&1 | ||
| 316 | RC=$? | ||
| 317 | set -e | ||
| 318 | [ "$RC" -ne 0 ] || { | ||
| 319 | echo "e2e FAIL: x forgets: a 33rd session was created on a full daemon," | ||
| 320 | echo " so the phantom-tile leg proved nothing:"; cat "$OUT.whph"; exit 1; } | ||
| 321 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 322 | echo "e2e FAIL: x forgets: a REFUSED first attach recorded a phantom tile:" | ||
| 323 | cat "$WHWALL"; exit 1; } | ||
| 324 | assert_stopped "$SOCK40" "$D37PID" "x forgets" "$OUT.whstop" | ||
| 325 | D37PID="" | ||
| 326 | ok "\\x1cx forgets a tile and leaves its session running; a refusal records nothing" | ||
| 327 | |||
| 328 | # ---- `mux TARGET` is a wall of one tile --------------------------------- | ||
| 329 | # | ||
| 330 | # The convergence's headline: `mux` dials and attaches exactly as it always | ||
| 331 | # did, and what it runs is the WALL — one tile whose rect is the whole | ||
| 332 | # terminal, no label bar, byte-identical to a plain client. `Ctrl-\ w` | ||
| 333 | # folds the SAVED wall into this one: it hydrates the file's tiles beside | ||
| 334 | # the tile this run is standing on, and what appears is THE wall — this | ||
| 335 | # run's tile plus the ones the saved file already holds. | ||
| 336 | # | ||
| 337 | # Four claims, and each needs a different kind of witness: | ||
| 338 | # | ||
| 339 | # * the wall `Ctrl-\ w` folds in is the SAVED one. A tile written by an | ||
| 340 | # EARLIER `mux` (the `#side` attach below, into this leg's own state | ||
| 341 | # home) paints a stripe here, in a process that was never told about | ||
| 342 | # it on its command line. A wall that only ever showed this run's own | ||
| 343 | # tile would satisfy every other assertion in this leg. | ||
| 344 | # * the focus stays on the entry tile, and that costs NOTHING at the | ||
| 345 | # daemon. The attach counter is sampled across the fold and the | ||
| 346 | # injection and moves only for the two real attaches (the entry tile | ||
| 347 | # and the saved tile the fold hydrated): focus is client-local and | ||
| 348 | # sends nothing on the wire. | ||
| 349 | # * `Ctrl-\ d` leaves MUX. There is no child and no second screen; the | ||
| 350 | # one-tile wall is the program, and the detach chord is the way out. | ||
| 351 | # * ...and the attach that opened all this earned exactly one line in the | ||
| 352 | # file, spelled `#0`, beside the `#side` that was already there. | ||
| 353 | # | ||
| 354 | # A state home of its own: the wall `Ctrl-\ w` folds in is the saved one, | ||
| 355 | # and the shared $XDG_STATE_HOME is every socket the suite has ever | ||
| 356 | # attached to. | ||
| 357 | start_daemon "$SOCK45" "$OUT.cv.d" "convergence daemon never bound" --shell /bin/sh | ||
| 358 | D45PID=$DPID | ||
| 359 | |||
| 360 | # The tile that is already on the wall before this leg's client starts. A | ||
| 361 | # piped `mux`, so its attach writes the line and then goes: what the leg | ||
| 362 | # asserts on is the FILE it left behind, not a process still holding it. | ||
| 363 | pipe_mux "$OUT.cvside" "$OUT.cvside.err" env XDG_STATE_HOME="$CVSTATE" timeout 40 "$MUX" --sock "$SOCK45" --session side | ||
| 364 | pipe_send 'printf "cvside-%%s\\n" pin\n' | ||
| 365 | await_out "$OUT.cvside" "cvside-pin" "cvside-pin never reached the client" | ||
| 366 | pipe_detach | ||
| 367 | wait_grid "$SOCK45" "cvside-pin" "convergence: the saved tile's marker" side | ||
| 368 | grep -qx -- "--sock $SOCK45#side" "$CVWALL" || { | ||
| 369 | echo "e2e FAIL: convergence: the setup attach wrote no tile to fold in:" | ||
| 370 | cat "$CVWALL" 2>&1; exit 1; } | ||
| 371 | |||
| 372 | # The whole run's budget, and every one of the three is named: the entry | ||
| 373 | # tile's attach, the saved tile's when the fold hydrates it, and the | ||
| 374 | # `muxa send` below, which is an attach like any other. The fold itself | ||
| 375 | # is a real attach (it dials the saved tile's session); the focus move and | ||
| 376 | # the leave add nothing, and a cumulative counter is what says so — a | ||
| 377 | # gauge would let a dial that opened and closed between two samples hide | ||
| 378 | # inside it. | ||
| 379 | CVATT_BEFORE=$(attaches_now "$SOCK45") | ||
| 380 | ( sleep 6; "$MUXA" send 'printf "cvret-%s\n" pin\n' \ | ||
| 381 | --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) & | ||
| 382 | CVINJPID=$! | ||
| 383 | defer_kill "$CVINJPID" | ||
| 384 | set +e | ||
| 385 | # tall: Ctrl-\ w folds in a saved tile, making two at 100x30 (100 >= 60); | ||
| 386 | # 70x36 stays stacked (70 < 72) and fits the bar label the assertion greps | ||
| 387 | XDG_STATE_HOME="$CVSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \ | ||
| 388 | --out "$OUT.cvcap" --err "$OUT.cvcap.err" \ | ||
| 389 | -- "$MUX" --sock "$SOCK45" > "$OUT.cvpc" 2>&1 <<'EOF' | ||
| 390 | expect \x1b[?1049h 15000 | ||
| 391 | settle 400 15000 | ||
| 392 | send printf 'cvhome-%s\\n' pin\n | ||
| 393 | expect cvhome-pin 15000 | ||
| 394 | settle 400 15000 | ||
| 395 | send \x1cw | ||
| 396 | expect cvside-pin 20000 | ||
| 397 | settle 900 20000 | ||
| 398 | expect cvret-pin 20000 | ||
| 399 | settle 500 15000 | ||
| 400 | send \x1cd | ||
| 401 | waitexit 10000 | ||
| 402 | EOF | ||
| 403 | RC=$? | ||
| 404 | set -e | ||
| 405 | wait "$CVINJPID" 2>/dev/null || true | ||
| 406 | [ "$RC" -eq 0 ] || { | ||
| 407 | echo "e2e FAIL: convergence: ptyclient leg exited $RC (did \\x1cw fold, did \\x1cd leave?):" | ||
| 408 | cat "$OUT.cvpc"; cat "$OUT.cvinj" 2>/dev/null; exit 1; } | ||
| 409 | # The saved tile really painted a STRIPE, which only a wall draws — a bar | ||
| 410 | # with a spelling and a state word in it. A bare marker grep would be | ||
| 411 | # satisfied by the setup attach's own capture bleeding through. | ||
| 412 | grep -q -- "--sock $SOCK45#side \[up\]" "$OUT.cvcap" || { | ||
| 413 | echo "e2e FAIL: convergence: the saved wall's tile never became a stripe:" | ||
| 414 | cat "$OUT.cvpc"; exit 1; } | ||
| 415 | grep -q -- "--sock $SOCK45#0 \[up\]" "$OUT.cvcap" || { | ||
| 416 | echo "e2e FAIL: convergence: the attached tile never became a stripe:" | ||
| 417 | cat "$OUT.cvpc"; exit 1; } | ||
| 418 | # The fold was a real attach (the saved tile dialled) and the injection was | ||
| 419 | # another; the focus move and the leave added nothing. THREE is the whole | ||
| 420 | # budget — entry tile, hydrated saved tile, injector — and a focus that | ||
| 421 | # re-dialled would make it four. | ||
| 422 | CVATT=$(attaches_now "$SOCK45") | ||
| 423 | assert_attach_delta "$CVATT_BEFORE" "$CVATT" 3 "convergence" | ||
| 424 | # The file: one line for the attach this run made, beside the one it found. | ||
| 425 | _cv_n=$(wc -l < "$CVWALL") | ||
| 426 | [ "$_cv_n" -eq 2 ] || { | ||
| 427 | echo "e2e FAIL: convergence: the wall file holds $_cv_n lines, want 2:" | ||
| 428 | cat "$CVWALL"; exit 1; } | ||
| 429 | grep -qx -- "--sock $SOCK45#0" "$CVWALL" || { | ||
| 430 | echo "e2e FAIL: convergence: the attach earned no tile of its own:" | ||
| 431 | cat "$CVWALL"; exit 1; } | ||
| 432 | assert_stopped "$SOCK45" "$D45PID" "convergence" "$OUT.cvstop" | ||
| 433 | D45PID="" | ||
| 434 | ok "mux is the wall: Ctrl-\\ w folds in the saved tiles, the focus stays put, Ctrl-\\ d leaves" | ||
| 435 | |||
| 436 | # ---- the ring GROWS the wall, and only when it has to -------------------- | ||
| 437 | # | ||
| 438 | # The spec's `n`/`p` rule, which phase 3c is where it lands: those chords | ||
| 439 | # move the focus around the DAEMON'S session ring, not around the wall's | ||
| 440 | # tiles. A sibling that already has a tile is an instant focus move; one | ||
| 441 | # that does not GETS one, because visiting it is an attach and attach adds. | ||
| 442 | # | ||
| 443 | # The leg is built so the two halves are the SAME chord pressed twice, and | ||
| 444 | # the difference between them is the whole assertion: | ||
| 445 | # | ||
| 446 | # * `\x1cn` from session 0 lands on `two`, which this run has never seen | ||
| 447 | # — its tile was written into a DIFFERENT state home by the setup | ||
| 448 | # client, so the daemon knows the session and this wall does not. One | ||
| 449 | # dial, one attach, one new line in the file, and the marker typed | ||
| 450 | # afterwards lands in `two`'s shell. | ||
| 451 | # * `\x1cn` again wraps back to 0, which now HAS a tile. Nothing is | ||
| 452 | # dialled: the counter must not move across that step, and the marker | ||
| 453 | # lands back in session 0. | ||
| 454 | # | ||
| 455 | # The attach budget for the run is therefore exactly 2 (the entry tile and | ||
| 456 | # the tile the first `n` grew), and the second `n` is the one that must be | ||
| 457 | # free. Sampled around the whole ptyclient run rather than around each | ||
| 458 | # step, because a step that dialled and closed between two samples would | ||
| 459 | # hide inside a gauge — the counter is cumulative and cannot. | ||
| 460 | start_daemon "$SOCK46" "$OUT.rg.d" "ring-grow daemon never bound" --shell /bin/sh | ||
| 461 | D46PID=$DPID | ||
| 462 | |||
| 463 | # `two` exists on the DAEMON but not in this leg's wall: a separate state | ||
| 464 | # home is what makes the setup client's own tile invisible here, so the | ||
| 465 | # first `n` meets a sibling with no tile — which is the case under test. | ||
| 466 | pipe_mux "$OUT.rgtwo" "$OUT.rgtwo.err" env XDG_STATE_HOME="$RGOTHER" timeout 40 "$MUX" --sock "$SOCK46" --session two | ||
| 467 | pipe_send 'printf "rgtwo-%%s\\n" pin\n' | ||
| 468 | await_out "$OUT.rgtwo" "rgtwo-pin" "rgtwo-pin never reached the client" | ||
| 469 | pipe_detach | ||
| 470 | wait_grid "$SOCK46" "rgtwo-pin" "ring grow: the untiled sibling's marker" two | ||
| 471 | grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" 2>/dev/null && { | ||
| 472 | echo "e2e FAIL: ring grow: the sibling already had a tile, so the ADD is vacuous:" | ||
| 473 | cat "$RGSTATE/mux/wall"; exit 1; } | ||
| 474 | |||
| 475 | RGATT_BEFORE=$(attaches_now "$SOCK46") | ||
| 476 | set +e | ||
| 477 | # tall: Ctrl-\ n grows the wall to two tiles at 100x30 (100 >= 60) | ||
| 478 | XDG_STATE_HOME="$RGSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 479 | --out "$OUT.rgcap" --err "$OUT.rgcap.err" \ | ||
| 480 | -- "$MUX" --sock "$SOCK46" > "$OUT.rgpc" 2>&1 <<'EOF' | ||
| 481 | expect \x1b[?1049h 15000 | ||
| 482 | settle 400 15000 | ||
| 483 | send printf 'rgzero-%s\\n' pin\n | ||
| 484 | expect rgzero-pin 15000 | ||
| 485 | settle 400 15000 | ||
| 486 | send \x1cn | ||
| 487 | expect rgtwo-pin 20000 | ||
| 488 | settle 500 15000 | ||
| 489 | send printf 'rghere-%s\\n' pin\n | ||
| 490 | expect rghere-pin 15000 | ||
| 491 | settle 400 15000 | ||
| 492 | send \x1cn | ||
| 493 | expect rgzero-pin 20000 | ||
| 494 | settle 500 15000 | ||
| 495 | send printf 'rgback-%s\\n' pin\n | ||
| 496 | expect rgback-pin 15000 | ||
| 497 | settle 400 15000 | ||
| 498 | send \x1c\x1c | ||
| 499 | waitexit 10000 | ||
| 500 | EOF | ||
| 501 | RC=$? | ||
| 502 | set -e | ||
| 503 | RGATT=$(attaches_now "$SOCK46") | ||
| 504 | [ "$RC" -eq 0 ] || { | ||
| 505 | echo "e2e FAIL: ring grow: ptyclient leg exited $RC (did \\x1cn step the ring?):" | ||
| 506 | cat "$OUT.rgpc"; exit 1; } | ||
| 507 | # TWO attaches for the whole run: the entry tile's, and the one the first | ||
| 508 | # `n` grew. A `n` that re-dialled the tile it already had would make it | ||
| 509 | # three, whichever way it closed the old one first. | ||
| 510 | assert_attach_delta "$RGATT_BEFORE" "$RGATT" 2 "ring grow" | ||
| 511 | # The wall grew by the truth: the sibling the ring visited earned a line. | ||
| 512 | grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" || { | ||
| 513 | echo "e2e FAIL: ring grow: the visited sibling earned no tile:" | ||
| 514 | cat "$RGSTATE/mux/wall" 2>&1; exit 1; } | ||
| 515 | _rg_n=$(wc -l < "$RGSTATE/mux/wall") | ||
| 516 | [ "$_rg_n" -eq 2 ] || { | ||
| 517 | echo "e2e FAIL: ring grow: the wall file holds $_rg_n lines, want 2:" | ||
| 518 | cat "$RGSTATE/mux/wall"; exit 1; } | ||
| 519 | # Where the markers landed says the focus really MOVED rather than the wall | ||
| 520 | # having gone on typing at one connection. Each session holds its own and | ||
| 521 | # not its neighbour's. | ||
| 522 | timeout 20 "$MUXA" capture --sock "$SOCK46" --session two > "$OUT.rgcapt" 2>&1 | ||
| 523 | grep -q "rghere-pin" "$OUT.rgcapt" || { | ||
| 524 | echo "e2e FAIL: ring grow: the marker typed after the first n missed session two:" | ||
| 525 | cat "$OUT.rgcapt"; exit 1; } | ||
| 526 | grep -q "rgback-pin" "$OUT.rgcapt" && { | ||
| 527 | echo "e2e FAIL: ring grow: the marker typed after the wrap landed in two:" | ||
| 528 | cat "$OUT.rgcapt"; exit 1; } | ||
| 529 | timeout 20 "$MUXA" capture --sock "$SOCK46" --session 0 > "$OUT.rgcap0" 2>&1 | ||
| 530 | grep -q "rgback-pin" "$OUT.rgcap0" || { | ||
| 531 | echo "e2e FAIL: ring grow: the wrap did not come back to session 0:" | ||
| 532 | cat "$OUT.rgcap0"; exit 1; } | ||
| 533 | assert_stopped "$SOCK46" "$D46PID" "ring grow" "$OUT.rgstop" | ||
| 534 | D46PID="" | ||
| 535 | ok "Ctrl-\\ n grows the wall for a sibling with no tile, and is free for one that has" | ||
| 536 | |||
| 537 | # ---- a session that ends in the focused tile --------------------------- | ||
| 538 | # | ||
| 539 | # The exit rule the convergence had to choose, stated in decisions.md and | ||
| 540 | # pinned here: a session that ends in the focused tile ENDS MUX when it was | ||
| 541 | # the wall's only tile, and LEAVES THE FOCUS TO A NEIGHBOUR when it was not. | ||
| 542 | # | ||
| 543 | # Both halves in one block, because either alone is satisfied by a bug: | ||
| 544 | # | ||
| 545 | # * one tile. `exit 7` in a piped `mux` — the shape every script in the | ||
| 546 | # world uses — and the code comes back 7. Graded against the mutation | ||
| 547 | # (`endAction` dropping to the wall whatever the tile count): `mux` | ||
| 548 | # comes back 0, because the wall it dropped to reads this script's | ||
| 549 | # closed stdin as a wall nobody is left to steer. Zero is not seven and | ||
| 550 | # the leg says so; 124 is the other shape the same bug can take. | ||
| 551 | # * two tiles. `\x1cc` adds one and focuses it, the shell in it exits, and | ||
| 552 | # the tile vanishes — a dead stripe for a cleanly exited shell is noise | ||
| 553 | # — leaving the survivor the whole screen. Then `\x1cd` leaves — exit | ||
| 554 | # 0, not 7 and not the shell's anything, because leaving is the user's | ||
| 555 | # act and not the session's. | ||
| 556 | # * two tiles and a PIPED client whose stdin has already gone. The case | ||
| 557 | # review reproduced as a hang, and the reason the rule's second half is | ||
| 558 | # stated in `endAction` rather than at the read that notices EOF. | ||
| 559 | start_daemon "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound" --shell /bin/sh | ||
| 560 | D47PID=$DPID | ||
| 561 | |||
| 562 | # A wall it dropped to instead would read as 124: it never leaves, because | ||
| 563 | # nothing closes its stdin under it. | ||
| 564 | pipe_mux "$OUT.xe1" "$OUT.xe1.err" env XDG_STATE_HOME="$XESTATE" timeout 40 \ | ||
| 565 | "$MUX" --sock "$SOCK47" | ||
| 566 | pipe_send 'exit 7\n' | ||
| 567 | pipe_waitexit "exit semantics: the only tile's shell exited 7, and mux" 7 | ||
| 568 | |||
| 569 | set +e | ||
| 570 | # tall: Ctrl-\ c creates a second tile at 100x30 (100 >= 60) | ||
| 571 | XDG_STATE_HOME="$XESTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 572 | --out "$OUT.xecap" --err "$OUT.xecap.err" \ | ||
| 573 | -- "$MUX" --sock "$SOCK47" > "$OUT.xepc" 2>&1 <<'EOF' | ||
| 574 | expect \x1b[?1049h 15000 | ||
| 575 | settle 400 15000 | ||
| 576 | send printf 'xehome-%s\\n' pin\n | ||
| 577 | expect xehome-pin 15000 | ||
| 578 | settle 400 15000 | ||
| 579 | send \x1cc | ||
| 580 | expect \x1b[2J 15000 | ||
| 581 | settle 600 15000 | ||
| 582 | send exit 3\n | ||
| 583 | expect xehome-pin 20000 | ||
| 584 | settle 800 20000 | ||
| 585 | send \x1cd | ||
| 586 | waitexit 10000 | ||
| 587 | EOF | ||
| 588 | RC=$? | ||
| 589 | set -e | ||
| 590 | [ "$RC" -eq 0 ] || { | ||
| 591 | echo "e2e FAIL: exit semantics: the multi-tile leg exited $RC (did the focus" | ||
| 592 | echo " move to the neighbour, and did \x1cd leave it?):" | ||
| 593 | cat "$OUT.xepc"; exit 1; } | ||
| 594 | # The exited tile vanished — a dead stripe for a cleanly exited shell is | ||
| 595 | # noise — so there is no `[exited]` bar to read, and the survivor took the | ||
| 596 | # whole screen: a one-tile wall draws no label bar. The marker it repainted | ||
| 597 | # is what `expect xehome-pin` above caught; the grid check below is the | ||
| 598 | # same fact from the daemon's side. | ||
| 599 | # The session that did NOT end is still there: a shell exiting in the | ||
| 600 | # focused tile ends that session and nothing else. | ||
| 601 | wait_grid "$SOCK47" "xehome-pin" "exit semantics: session 0 outlived its neighbour" | ||
| 602 | |||
| 603 | # The third case, and the one review REPRODUCED as a hang: two tiles, but | ||
| 604 | # a PIPED client whose stdin has already closed when the focused session | ||
| 605 | # ends. Moving the focus to a neighbour is not an answer there — a wall | ||
| 606 | # nobody is left to steer has no keyboard, so mux waited in poll forever | ||
| 607 | # with the exit code in hand (RC=124, measured). The rule is the same rule | ||
| 608 | # with its second half stated: an ending nobody can leave is mux's ending. | ||
| 609 | # | ||
| 610 | # The gap is built rather than raced: the shell reads `sleep 4` and | ||
| 611 | # `exit 3` from a stdin that closes IMMEDIATELY after, so the pipe is long | ||
| 612 | # gone by the time the session ends four seconds later. `timeout` is what | ||
| 613 | # turns a regression into a failure instead of a wedged suite. | ||
| 614 | XETIME_START=$(date +%s) | ||
| 615 | set +e | ||
| 616 | { sleep 1; printf '\034c'; sleep 3; printf 'sleep 4\n'; printf 'exit 3\n'; } | \ | ||
| 617 | XDG_STATE_HOME="$XESTATE" timeout 40 "$MUX" --sock "$SOCK47" \ | ||
| 618 | > "$OUT.xe2" 2> "$OUT.xe2.err" | ||
| 619 | RC=$? | ||
| 620 | set -e | ||
| 621 | XETIME=$(( $(date +%s) - XETIME_START )) | ||
| 622 | [ "$RC" -eq 3 ] || { | ||
| 623 | echo "e2e FAIL: exit semantics: a piped two-tile client whose session ended" | ||
| 624 | echo " exited $RC after ${XETIME}s, want 3 — 124 is the wall it" | ||
| 625 | echo " left to a neighbour with nobody left to steer it:" | ||
| 626 | cat "$OUT.xe2.err"; exit 1; } | ||
| 627 | # Promptly, not eventually: the code is right in both readings, but a | ||
| 628 | # client that only exits because the timeout reaped it is the bug wearing | ||
| 629 | # the right answer. The session ends ~8s in; 20 is generous and still far | ||
| 630 | # under the 40s timeout that would otherwise be the only bound. | ||
| 631 | [ "$XETIME" -lt 20 ] || { | ||
| 632 | echo "e2e FAIL: exit semantics: the piped client took ${XETIME}s to notice its" | ||
| 633 | echo " own session had ended — it exited, but it did not notice"; exit 1; } | ||
| 634 | assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop" | ||
| 635 | D47PID="" | ||
| 636 | ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour" | ||