a73x

0c07bcf8

test: e2e — a wall is a daemon, not a list of sessions; pin 81

a73x   2026-08-28 19:53

Commit message
test: e2e — a wall is a daemon, not a list of sessions; pin 81

`mux wall SPELLING...` is gone, so every leg that spelled its tiles now
names a DAEMON and lets the wall find the sessions. The substitution is
mechanical and the assertions are unchanged wherever they could be, but
three consequences were not optional:

  * a daemon is born holding `0`, so a wall over it always has that tile.
    A leg that wanted two tiles used to make `a` and `b`; it now uses the
    daemon's own `0` and one named `b`, or it would be a three-tile leg
    pretending to be a two-tile one.
  * a wall is the HOSTS FILE, and the suite's shared `$XDG_STATE_HOME`
    is every daemon it has ever attached to. Each of these legs gets a
    state home of its own, or it opens on a wall of other legs' corpses.
  * every wall SAVES a sidecar on its way out, including the piped
    attaches that set a leg up. A leg that asserts the default cut —
    the aspect rule, a rail column, a stripe's rows — removes the saved
    tree first, because the aspect rule applies only when there is none.

What could not be kept:

  * `mux wall`'s self-refusal (07): there is no `mux wall`, and a `#`
    spelling is refused by the grammar before any wall exists — which is
    what the leg now asserts, from inside a session shell as before.
  * `Ctrl-\ c` against a FULL DAEMON (07): `max_tiles` and `max_sessions`
    are both 32, so a wall of one host runs out of stripes before its
    daemon runs out of slots and the daemon's refusal is unreachable from
    a single-host wall. The leg now saturates the TERMINAL — eight
    sessions on 24 rows — and asserts the same recovery: no session
    created, the focus where it was.
  * the dead-tile leg (08): a tile can no longer be spelled, so a
    nonexistent session cannot be named. A host that is down is one
    `[unreachable]` stripe, which 09 scenario 4 asserts.
  * hydrated creates (13): a wall line names a daemon, so no saved line
    can resurrect a session. 09 scenario 4 asserts the other half — that
    a restarted daemon re-creates nothing.
  * `Ctrl-\ :` (13) takes a HOST spelling: the leg adds the second daemon
    and its session appears as a tile, painting output that daemon's
    shell produced before this client existed.
  * layout healing (12) is now LIVE drift — a session ends and another is
    born between two runs — because a hosts file has no session lines to
    edit.

The pin: 83 - 1 (08's dead tile) - 1 (13's hydrated creates) = 81, and
every `ok` in this suite is unconditional at column 0, so the count is
also `grep -c '^ok "'` over the group files.

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" = "83" ] || { 180 [ "$OK_COUNT" = "81" ] || {
181 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 83 —" 181 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 81 —"
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_07_wallcli.sh
Old New
@@ -2,25 +2,34 @@
2 # e2e_07_wallcli.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in 2 # e2e_07_wallcli.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 3 # the order they stand in; see the lib's header for what this file may
4 # assume and what it must register. 4 # assume and what it must register.
5 # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be 5 # The CLI wall: its own daemon, so its sessions can't be confused with any
6 # confused with any other block's. 6 # other block's, and a state home of its own, because a wall is now the
7 # HOSTS FILE and the shared one is every daemon this suite has attached to.
7 SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock" 8 SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock"
8 defer_sock "$SOCK26" 9 defer_sock "$SOCK26"
10 CWSTATE="${TMPDIR:-/tmp}/mux-e2e-cliwall-state-$$"
11 defer_rm "$CWSTATE"
9 # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord 12 # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord
10 # that fails to detach cannot strand another block's session. 13 # that fails to detach cannot strand another block's session.
11 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" 14 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock"
12 defer_sock "$SOCK27" 15 defer_sock "$SOCK27"
16 PFXSTATE="${TMPDIR:-/tmp}/mux-e2e-prefix-state-$$"
17 defer_rm "$PFXSTATE"
13 # Ctrl-\ c (create a session and switch to it): its own daemon, because the 18 # Ctrl-\ c (create a session and switch to it): its own daemon, because the
14 # assertion is about which sessions EXIST — another block's session on the 19 # assertion is about which sessions EXIST — another block's session on the
15 # same daemon would change the name the chord picks. 20 # same daemon would change the name the chord picks.
16 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock" 21 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock"
17 defer_sock "$SOCK28" 22 defer_sock "$SOCK28"
23 NSWSTATE="${TMPDIR:-/tmp}/mux-e2e-newsess-state-$$"
24 defer_rm "$NSWSTATE"
18 # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same 25 # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same
19 # reason as SOCK28, and more sharply — the ring IS the list of sessions on 26 # reason as SOCK28, and more sharply — the ring IS the list of sessions on
20 # one daemon, so a stray session from another block would change where a 27 # one daemon, so a stray session from another block would change where a
21 # step lands. 28 # step lands.
22 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" 29 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock"
23 defer_sock "$SOCK29" 30 defer_sock "$SOCK29"
31 RINGSTATE="${TMPDIR:-/tmp}/mux-e2e-ring-state-$$"
32 defer_rm "$RINGSTATE"
24 # M5 (the self-attach refusal): its own daemon because the assertion reads 33 # M5 (the self-attach refusal): its own daemon because the assertion reads
25 # the SOCKET PATH out of a session shell's environment and compares it to 34 # the SOCKET PATH out of a session shell's environment and compares it to
26 # the one this block started — a shared daemon would make the comparison a 35 # the one this block started — a shared daemon would make the comparison a
@@ -29,13 +38,13 @@ SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
29 defer_sock "$SOCK31" 38 defer_sock "$SOCK31"
30 39
31 # ---- the CLI wall: the hub's multiattach, in one terminal -------------- 40 # ---- the CLI wall: the hub's multiattach, in one terminal --------------
32 # One daemon, two named sessions, one `mux wall` on a real pty showing 41 # One daemon, two named sessions, one `mux` on a real pty showing both AT
33 # both AT ONCE — the browser wall's claim, made by the CLI. Three facts, 42 # ONCE — the browser wall's claim, made by the CLI. Three facts, each with
34 # each with its own witness: both sessions' markers painted (the snapshot 43 # its own witness: both sessions' markers painted (the snapshot path), a
35 # path), a line injected into b WHILE the wall is attached painted too 44 # line injected into b WHILE the wall is attached painted too (the delta
36 # (the delta path — the wall is a live replica, not a capture), and 45 # path — the wall is a live replica, not a capture), and `\x1cd` ended the
37 # `\x1cd` ended the process with exit 0 (the wall left, not died). Markers 46 # process with exit 0 (the wall left, not died). Markers are
38 # are shell-EXPANDED (`%s`+pin), the M18 trick: a hit is the shell's work, 47 # shell-EXPANDED (`%s`+pin), the M18 trick: a hit is the shell's work,
39 # never an echo of anything typed here. 48 # never an echo of anything typed here.
40 # 49 #
41 # The one expect anchors on the injected delta, deliberately: tile pumps 50 # The one expect anchors on the injected delta, deliberately: tile pumps
@@ -45,18 +54,19 @@ defer_sock "$SOCK31"
45 # replica that must already hold b's snapshot); the snapshot markers are 54 # replica that must already hold b's snapshot); the snapshot markers are
46 # asserted order-free by the greps on the capture below. 55 # asserted order-free by the greps on the capture below.
47 # 56 #
48 # The two tiles are spelled DIFFERENTLY on purpose: `--sock PATH` as two 57 # Nothing on the command line names a tile any more: the wall is the
49 # arguments (muxweb's dialect) and as one quoted spelling (the wall file's). 58 # DAEMON, and the tiles are whatever sessions it turns out to have. `a` is
50 # Both must reach the same tile, so both are pinned by this one leg. 59 # named only because the wall is entered zoomed on it; `b` and the
60 # daemon's own `0` arrive from the host's list, which is the claim.
51 start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh 61 start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh
52 D23PID=$DPID 62 D23PID=$DPID
53 63
54 pipe_mux "$OUT.cwa" "$OUT.cwa.err" timeout 40 "$MUX" --sock "$SOCK26" --session a 64 pipe_mux "$OUT.cwa" "$OUT.cwa.err" env XDG_STATE_HOME="$CWSTATE" timeout 40 "$MUX" --sock "$SOCK26" --session a
55 pipe_send 'printf "cwa-%%s\\n" pin\n' 65 pipe_send 'printf "cwa-%%s\\n" pin\n'
56 await_out "$OUT.cwa" "cwa-pin" "cwa-pin never reached the client" 66 await_out "$OUT.cwa" "cwa-pin" "cwa-pin never reached the client"
57 pipe_detach 67 pipe_detach
58 wait_grid "$SOCK26" "cwa-pin" "CLI wall: session a's marker" a 68 wait_grid "$SOCK26" "cwa-pin" "CLI wall: session a's marker" a
59 pipe_mux "$OUT.cwb" "$OUT.cwb.err" timeout 40 "$MUX" --sock "$SOCK26" --session b 69 pipe_mux "$OUT.cwb" "$OUT.cwb.err" env XDG_STATE_HOME="$CWSTATE" timeout 40 "$MUX" --sock "$SOCK26" --session b
60 pipe_send 'printf "cwb-%%s\\n" pin\n' 70 pipe_send 'printf "cwb-%%s\\n" pin\n'
61 await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client" 71 await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client"
62 pipe_detach 72 pipe_detach
@@ -67,8 +77,13 @@ wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b
67 CWINJPID=$! 77 CWINJPID=$!
68 defer_kill "$CWINJPID" 78 defer_kill "$CWINJPID"
69 set +e 79 set +e
70 timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \ 80 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
71 "$MUX" wall --sock "$SOCK26#a" "--sock $SOCK26#b" > "$OUT.cwpc" 2>&1 <<'EOF' 81 # came before it saved a sidecar on its way out. The aspect rule applies
82 # only when nothing is saved.
83 rm -f "$CWSTATE/mux/layout"
84 XDG_STATE_HOME="$CWSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 \
85 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \
86 "$MUX" --sock "$SOCK26" --session a > "$OUT.cwpc" 2>&1 <<'EOF'
72 expect cwlive-pin 20000 87 expect cwlive-pin 20000
73 send \x1cd 88 send \x1cd
74 waitexit 10000 89 waitexit 10000
@@ -93,7 +108,7 @@ grep -q "cwlive-pin" "$OUT.cwcap" || {
93 # assert — the wall writes the sessions it shows. 108 # assert — the wall writes the sessions it shows.
94 assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop" 109 assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop"
95 D23PID="" 110 D23PID=""
96 ok "mux wall: two sessions and a live delta on one terminal" 111 ok "mux --sock: every session the daemon has, and a live delta, on one terminal"
97 112
98 # ---- M1: Ctrl-\ is a prefix, not an action ------------------------------ 113 # ---- M1: Ctrl-\ is a prefix, not an action ------------------------------
99 # 114 #
@@ -117,7 +132,12 @@ start_daemon "$SOCK27" "$OUT.pfx.d" "prefix daemon never bound" --shell /bin/sh
117 D24PID=$DPID 132 D24PID=$DPID
118 133
119 set +e 134 set +e
120 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pfx" --err "$OUT.pfx.err" \ 135 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
136 # came before it saved a sidecar on its way out. The aspect rule applies
137 # only when nothing is saved.
138 rm -f "$PFXSTATE/mux/layout"
139 XDG_STATE_HOME="$PFXSTATE" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \
140 --out "$OUT.pfx" --err "$OUT.pfx.err" \
121 -- "$MUX" --sock "$SOCK27" > "$OUT.pfx.log" 2>&1 <<'EOF' 141 -- "$MUX" --sock "$SOCK27" > "$OUT.pfx.log" 2>&1 <<'EOF'
122 expect \x1b[?1049h 15000 142 expect \x1b[?1049h 15000
123 settle 400 15000 143 settle 400 15000
@@ -147,7 +167,7 @@ assert_stopped "$SOCK27" "$D24PID" "prefix" "$OUT.pfxstop"
147 D24PID="" 167 D24PID=""
148 ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches" 168 ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches"
149 169
150 # ---- M2: Ctrl-\ c creates a session and switches to it ------------------- 170 # ---- M2: Ctrl-\ c creates a session and gives it a tile -------------------
151 # 171 #
152 # The claim is a MOVE, which takes two witnesses: the client ends up 172 # The claim is a MOVE, which takes two witnesses: the client ends up
153 # somewhere new, and the place it left is still there and unchanged. 173 # somewhere new, and the place it left is still there and unchanged.
@@ -162,38 +182,32 @@ ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches"
162 # second. Without that pair the same log would be produced by a chord 182 # second. Without that pair the same log would be produced by a chord
163 # that did nothing at all. 183 # that did nothing at all.
164 # 184 #
165 # The leg then keeps pressing until the daemon runs out of sessions, because 185 # The arrival signal is the new tile's own LABEL BAR. `\x1cc` used to tear
166 # the refusal is the branch with the interesting answer: a refused attach is 186 # the terminal down and build it back up — a fresh `?1049h` per switch —
167 # the one failure the zoom recovers from instead of exiting, and nothing 187 # because each switch was a new client process; then it became a zoom, and
168 # else in the suite reaches it. See the block after the leg for the three 188 # the signal was `setFocus`'s `\x1b[2J`. Now the chord ADDS A TILE beside
169 # things "recovered" has to mean. 189 # the focus and leaves both on screen, so neither happens: what is new on
190 # the terminal is a second stripe whose marker carries the focus (`2> `,
191 # `paintLabel`). A one-tile wall draws no bar at all, so that byte sequence
192 # cannot be anything but the tile the chord just made.
170 # 193 #
171 # PHASE 3C evolved the WAIT, and only the wait. `\x1cc` used to tear the 194 # Then the refusal, which is the branch with the interesting answer: a
172 # terminal down and build it back up — a fresh `?1049h` per switch — because 195 # birth the WALL has no room for. The daemon is not full here and could not
173 # each switch was a new client process on a new transport. Now `mux` IS the 196 # usefully be — `max_tiles` and `max_sessions` are both 32, so a wall of one
174 # wall: the chord adds a tile and moves the ZOOM to it, and the alternate 197 # host runs out of stripes before its daemon runs out of slots, and the
175 # screen was entered once, at startup, and is never left. So the arrival 198 # terminal runs out of rows long before either. Eight sessions on a 24-row
176 # signal is the zoom's own: `setFocus` clears the screen (`\x1b[2J`) on the 199 # terminal is a saturated wall exactly (three rows a stripe: two for the
177 # thread that moves it, before the terminal changes hands. 200 # daemon's floor, one for the bar), and a ninth has nowhere to go.
178 #
179 # That needle is not merely "some paint": between the chord and the answer
180 # the old tile is idle and paints nothing, so the next `[2J` after the chord
181 # is the handover itself. Waiting for it MATTERS — keystrokes sent before it
182 # would go to the mailbox of the tile the user is leaving, which is the
183 # session they were typing in a moment ago and would silently swallow the
184 # marker.
185 start_daemon "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" --shell /bin/sh 201 start_daemon "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" --shell /bin/sh
186 D25PID=$DPID 202 D25PID=$DPID
187 # The refusal this leg's fourth chord asserts needs a FULL table. The
188 # chords make four (0, 1, 2, 3); the other 28 are filled here. `fillN` is
189 # not an integer, so nextFreeName still hands the chords 1, 2, 3 and
190 # reaches for "4" on the refused one — which is the name the post-check
191 # below asks about.
192 fill_sessions "$SOCK28" "$OUT.nswfill" fill 4 31
193 203
194 set +e 204 set +e
195 # tall: Ctrl-\ c adds a second tile at 80x24 (80 >= 48) 205 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
196 timeout 40 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.nsw" --err "$OUT.nsw.err" \ 206 # came before it saved a sidecar on its way out. The aspect rule applies
207 # only when nothing is saved.
208 rm -f "$NSWSTATE/mux/layout"
209 XDG_STATE_HOME="$NSWSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 24 \
210 --out "$OUT.nsw" --err "$OUT.nsw.err" \
197 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF' 211 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF'
198 expect \x1b[?1049h 15000 212 expect \x1b[?1049h 15000
199 settle 400 15000 213 settle 400 15000
@@ -201,33 +215,18 @@ send printf 'm2a-%s\\n' pin\n
201 expect m2a-pin 15000 215 expect m2a-pin 15000
202 settle 400 15000 216 settle 400 15000
203 send \x1cc 217 send \x1cc
204 expect \x1b[2J 15000 218 expect 2> --sock 15000
205 settle 400 15000 219 settle 600 15000
206 send printf 'm2b-%s\\n' pin\n 220 send printf 'm2b-%s\\n' pin\n
207 expect m2b-pin 15000 221 expect m2b-pin 15000
208 settle 400 15000 222 settle 400 15000
209 send \x1cc
210 expect \x1b[2J 15000
211 settle 400 15000
212 send \x1cc
213 expect \x1b[2J 15000
214 settle 400 15000
215 send printf 'm2c-%s\\n' pin\n
216 expect m2c-pin 15000
217 settle 400 15000
218 send \x1cc
219 expect m2c-pin 15000
220 settle 400 15000
221 send printf 'm2d-%s\\n' pin\n
222 expect m2d-pin 15000
223 settle 400 15000
224 send \x1c\x1c 223 send \x1c\x1c
225 waitexit 10000 224 waitexit 10000
226 EOF 225 EOF
227 RC=$? 226 RC=$?
228 set -e 227 set -e
229 [ "$RC" -eq 0 ] || { 228 [ "$RC" -eq 0 ] || {
230 echo "e2e FAIL: new session: ptyclient leg exited $RC (did \\x1cc switch?):" 229 echo "e2e FAIL: new session: ptyclient leg exited $RC (did \\x1cc make a tile?):"
231 cat "$OUT.nsw.log"; exit 1; } 230 cat "$OUT.nsw.log"; exit 1; }
232 "$MUXA" status --sock "$SOCK28" --session 1 > "$OUT.nswst" 2>&1 || { 231 "$MUXA" status --sock "$SOCK28" --session 1 > "$OUT.nswst" 2>&1 || {
233 echo "e2e FAIL: new session: the chord did not create session 1:" 232 echo "e2e FAIL: new session: the chord did not create session 1:"
@@ -241,44 +240,78 @@ grep -q "m2a-pin" "$OUT.nswcap" || {
241 grep -q "m2b-pin" "$OUT.nswcap" && { 240 grep -q "m2b-pin" "$OUT.nswcap" && {
242 echo "e2e FAIL: new session: the second marker ran in the OLD session" 241 echo "e2e FAIL: new session: the second marker ran in the OLD session"
243 cat "$OUT.nswcap"; exit 1; } 242 cat "$OUT.nswcap"; exit 1; }
244 # The fourth chord had nowhere to go: the fill above took 28 slots and the 243 "$MUXA" capture --sock "$SOCK28" --session 1 > "$OUT.nswcap1" 2>&1
245 # chords the other four (0, 1, 2, 3), so the daemon answered the attach with an 244 grep -q "m2b-pin" "$OUT.nswcap1" || {
246 # exit_status before a single frame of state. That refusal is NOT fatal — 245 echo "e2e FAIL: new session: the chord's own session never got the marker:"
247 # the tile the chord created knows which tile it was born from — and the 246 cat "$OUT.nswcap1"; exit 1; }
248 # three claims below are what "fell back" means, none of which the log 247
249 # shows on its own. 248 # Six more, so the wall the next client opens is eight tiles on 24 rows —
249 # saturated, with nothing left over. Exactly saturated on purpose: a wall
250 # with sessions it cannot place says `[+N not shown]` on every poll, and
251 # that notice would overwrite the one this leg is waiting to read.
252 fill_sessions "$SOCK28" "$OUT.nswfill" fill 2 7
253 wait_sessions "$SOCK28" 8 "new session: the wall's terminal should be exactly full"
254
255 set +e
256 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
257 # came before it saved a sidecar on its way out. The aspect rule applies
258 # only when nothing is saved.
259 rm -f "$NSWSTATE/mux/layout"
260 XDG_STATE_HOME="$NSWSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 24 \
261 --out "$OUT.nsw2" --err "$OUT.nsw2.err" \
262 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw2.log" 2>&1 <<'EOF'
263 expect m2a-pin 25000
264 settle 2500 25000
265 send \x1cc
266 settle 1500 15000
267 send printf 'm2d-%s\\n' pin\n
268 expect m2d-pin 15000
269 settle 400 15000
270 # ...and then a focus move, which is what makes the refusal SPEAKABLE: the
271 # notice is left in `shared` for whichever Core claims the terminal next,
272 # so a refusal that moves no focus (the two sends above) paints nothing.
273 # `n` claims a neighbour, that claim takes the notice, and the banner is
274 # the sentence. `expect`, so a wall that went silent fails here rather than
275 # in a grep after the run.
276 send \x1cn
277 expect no room on the wall 15000
278 send \x1c\x1c
279 waitexit 10000
280 EOF
281 RC=$?
282 set -e
283 [ "$RC" -eq 0 ] || {
284 echo "e2e FAIL: new session: the saturated-wall leg exited $RC:"
285 cat "$OUT.nsw2.log"; exit 1; }
286 # The refusal is NOT fatal, and the three claims below are what "recovered"
287 # means — none of which the log shows on its own.
250 # 288 #
251 # It is also why the fourth chord's wait is `expect m2c-pin` and not the 289 # * the user was told. The sentence is read off the STREAM and not off a
252 # `[2J` the other three use: a refused tile's screen is cleared like any 290 # rendered grid: a banner is a transient overwrite that the next repaint
253 # other, so `[2J` cannot tell "the zoom arrived" from "the zoom came 291 # takes back, so the final screen is not where it lives. Nothing else in
254 # straight back". Session 3's own marker, repainted from a replica that was 292 # this suite spells this string, so a match is the client having said it.
255 # hot the whole time, can — and it is the same positive the capture
256 # assertion below leans on.
257 # 293 #
258 # * the client said so, once, in its own words. Its stderr is ptyclient's 294 # * the focus never left the tile the chord was typed in: the marker run
259 # --err file, not the pty, so the message is read there. 295 # AFTER the refusal is in the default session's grid, beside the one
260 grep -q "cannot create a new session" "$OUT.nsw.err" || { 296 # from before it. A capture holding only m2a-pin would equally describe
261 echo "e2e FAIL: new session: the refused switch said nothing:" 297 # a client that died there.
262 cat "$OUT.nsw.err"; exit 1; } 298 grep -qF -- "[no room on the wall for another tile]" "$OUT.nsw2" || {
263 # * it is back in session 3, the one it typed the chord in — proved 299 echo "e2e FAIL: new session: the refused chord said nothing — the notice"
264 # POSITIVELY, by a marker run AFTER the refusal landing in that 300 echo " never reached the terminal on the next claim:"
265 # session's grid beside the one from before it. A capture that held 301 cat "$OUT.nsw2.log"; exit 1; }
266 # only m2c-pin would equally describe a client that died there. 302 "$MUXA" capture --sock "$SOCK28" > "$OUT.nswcapd" 2>&1
267 "$MUXA" capture --sock "$SOCK28" --session 3 > "$OUT.nswcap3" 2>&1 303 grep -q "m2d-pin" "$OUT.nswcapd" || {
268 grep -q "m2c-pin" "$OUT.nswcap3" || { 304 echo "e2e FAIL: new session: the refused chord did not come back to the focused session:"
269 echo "e2e FAIL: new session: session 3 lost the marker from before the refusal:" 305 cat "$OUT.nswcapd"; exit 1; }
270 cat "$OUT.nswcap3"; exit 1; }
271 grep -q "m2d-pin" "$OUT.nswcap3" || {
272 echo "e2e FAIL: new session: the refused switch did not come back to session 3:"
273 cat "$OUT.nswcap3"; exit 1; }
274 # * and the name it was reaching for was never created. Without this the 306 # * and the name it was reaching for was never created. Without this the
275 # leg would pass on a daemon that quietly grew one more slot. 307 # leg would pass on a wall that refused the tile and made the session
276 "$MUXA" status --sock "$SOCK28" --session 4 > "$OUT.nswst4" 2>&1 && { 308 # anyway — a session nobody can see is the worse of the two failures.
277 echo "e2e FAIL: new session: the daemon created a 33rd session:" 309 "$MUXA" status --sock "$SOCK28" --session 2 > "$OUT.nswst2" 2>&1 && {
278 cat "$OUT.nswst4"; exit 1; } 310 echo "e2e FAIL: new session: a refused tile left a session behind:"
311 cat "$OUT.nswst2"; exit 1; }
279 assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop" 312 assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop"
280 D25PID="" 313 D25PID=""
281 ok "Ctrl-\\ c: a new session is created and switched to, the old one intact; a full daemon refuses and the client stays where it was" 314 ok "Ctrl-\\ c: a new session is created and given a tile, the old one intact; a full wall refuses and the client stays where it was"
282 315
283 # ---- M3: Ctrl-\ n / Ctrl-\ p step around the session ring ---------------- 316 # ---- M3: Ctrl-\ n / Ctrl-\ p step around the session ring ----------------
284 # 317 #
@@ -318,7 +351,12 @@ D26PID=$DPID
318 351
319 set +e 352 set +e
320 # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48) 353 # tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48)
321 timeout 60 "$PTYCLIENT" --cols 40 --rows 24 --out "$OUT.ring" --err "$OUT.ring.err" \ 354 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
355 # came before it saved a sidecar on its way out. The aspect rule applies
356 # only when nothing is saved.
357 rm -f "$RINGSTATE/mux/layout"
358 XDG_STATE_HOME="$RINGSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 24 \
359 --out "$OUT.ring" --err "$OUT.ring.err" \
322 -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF' 360 -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF'
323 expect \x1b[?1049h 15000 361 expect \x1b[?1049h 15000
324 settle 400 15000 362 settle 400 15000
@@ -385,8 +423,9 @@ ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends"
385 # this milestone that read true, because an inner client really had 423 # this milestone that read true, because an inner client really had
386 # taken the screen. A message grep alone would still pass if the client 424 # taken the screen. A message grep alone would still pass if the client
387 # printed the line and attached anyway. 425 # printed the line and attached anyway.
388 # 3. `mux wall` refuses a tile that is the session it is running in, by 426 # 3. a wall spelling is a DAEMON, so the session this shell is inside
389 # name. A wall that silently dropped it would be a wall that lies. 427 # cannot be named on the command line at all: `mux hosts add` refuses
428 # the `#` by name.
390 # 429 #
391 # Every marker is assembled by printf from pieces and never typed whole, so 430 # Every marker is assembled by printf from pieces and never typed whole, so
392 # the grid's echo of the command line can never satisfy the grep. `; echo` 431 # the grid's echo of the command line can never satisfy the grep. `; echo`
@@ -436,18 +475,21 @@ grep -q '"alt_screen":false' "$OUT.sast" || {
436 echo "e2e FAIL: self-attach: an inner client took the alternate screen — the loop ran:" 475 echo "e2e FAIL: self-attach: an inner client took the alternate screen — the loop ran:"
437 cat "$OUT.sast"; exit 1; } 476 cat "$OUT.sast"; exit 1; }
438 477
439 # The wall's spelling is ONE argument, quotes and all (the wall grammar, 478 # The wall's own half of the rule, and it is a SILENCE rather than a
440 # wall.zig) — the same string scenario 33 puts in the state file. 479 # refusal: `mux hosts add` names a DAEMON, so the session this shell is
480 # inside cannot be spelled at all — the wall simply never births a tile for
481 # it (`planHostDiff`'s `self_name`). A `#` spelling is refused by the
482 # grammar before any of that, which is scenario 09's leg.
441 timeout 20 "$MUXA" send \ 483 timeout 20 "$MUXA" send \
442 "$MUXABS wall '--sock $SOCK31#0'; printf 'WALL%s-%s' rc \$?; echo\n" \ 484 "$MUXABS hosts add '--sock $SOCK31#0'; printf 'WALL%s-%s' rc \$?; echo\n" \
443 --sock "$SOCK31" --session 0 > "$OUT.sawall" 2>&1 485 --sock "$SOCK31" --session 0 > "$OUT.sawall" 2>&1
444 sleep 2 486 sleep 2
445 timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap3" 2>&1 487 timeout 20 "$MUXA" capture --sock "$SOCK31" --session 0 > "$OUT.sacap3" 2>&1
446 grep -q -- "mux: wall target '--sock $SOCK31#0' is the session this shell is inside" "$OUT.sacap3" || { 488 grep -q -- "names a session after" "$OUT.sacap3" || {
447 echo "e2e FAIL: self-attach: mux wall struck its own tile without naming it:" 489 echo "e2e FAIL: self-attach: 'mux hosts add' took a session spelling:"
448 cat "$OUT.sawall"; cat "$OUT.sacap3"; exit 1; } 490 cat "$OUT.sawall"; cat "$OUT.sacap3"; exit 1; }
449 grep -q "WALLrc-2" "$OUT.sacap3" || { 491 grep -q "WALLrc-2" "$OUT.sacap3" || {
450 echo "e2e FAIL: self-attach: the wall refusal did not exit 2 (or did not exit):" 492 echo "e2e FAIL: self-attach: the hosts refusal did not exit 2 (or did not exit):"
451 cat "$OUT.sacap3"; exit 1; } 493 cat "$OUT.sacap3"; exit 1; }
452 assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop" 494 assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop"
453 D28PID="" 495 D28PID=""
test/e2e_08_mouse.sh
Old New
@@ -33,11 +33,11 @@ defer_sock "$SOCK36"
33 # on this daemon — would make both numbers say nothing. 33 # on this daemon — would make both numbers say nothing.
34 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock" 34 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock"
35 defer_sock "$SOCK37" 35 defer_sock "$SOCK37"
36 # The dead tile. Its own daemon because the leg needs a session name that 36 # ...and a state home of its own for the same reason one step further out:
37 # does NOT exist — the daemon refuses the attach — and any other block's 37 # a wall is the HOSTS FILE now, and the shared state home is every daemon
38 # daemon might have one by that name. 38 # this suite has ever attached to, each of them a stripe.
39 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock" 39 ZSSTATE="${TMPDIR:-/tmp}/mux-e2e-focusskip-state-$$"
40 defer_sock "$SOCK39" 40 defer_rm "$ZSSTATE"
41 # The wheel inside the focused tile. Two daemons for the wheel pair's own 41 # The wheel inside the focused tile. Two daemons for the wheel pair's own
42 # reason, turned up one notch: each needs a session whose SHELL is fixed for 42 # reason, turned up one notch: each needs a session whose SHELL is fixed for
43 # the session's whole life — one that scrolls and one that ASKS for the 43 # the session's whole life — one that scrolls and one that ASKS for the
@@ -46,10 +46,14 @@ defer_sock "$SOCK39"
46 # spelling is a bare `--sock PATH` and nothing has to create anything. 46 # spelling is a bare `--sock PATH` and nothing has to create anything.
47 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock" 47 SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock"
48 defer_sock "$SOCK41" 48 defer_sock "$SOCK41"
49 ZWSTATE="${TMPDIR:-/tmp}/mux-e2e-wallwheel-state-$$"
50 defer_rm "$ZWSTATE"
49 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh" 51 ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh"
50 defer_rm "$ZWHEELSH" 52 defer_rm "$ZWHEELSH"
51 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock" 53 SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
52 defer_sock "$SOCK42" 54 defer_sock "$SOCK42"
55 ZM2STATE="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-state-$$"
56 defer_rm "$ZM2STATE"
53 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh" 57 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
54 defer_rm "$ZMOUSESH" 58 defer_rm "$ZMOUSESH"
55 59
@@ -314,34 +318,40 @@ ok "a client with no terminal of its own forwards SGR-shaped bytes untouched"
314 # 318 #
315 # One ptyclient leg, two sessions' worth of typing on two tiles: 319 # One ptyclient leg, two sessions' worth of typing on two tiles:
316 # 320 #
317 # type mark a with zs-one (a is focused from the first byte) 321 # type mark 0 with zs-one (the entry tile is focused from the
322 # first byte)
318 # Ctrl-\ n move the focus to tile 2 (session b) 323 # Ctrl-\ n move the focus to tile 2 (session b)
319 # type mark b with zs-two 324 # type mark b with zs-two
320 # Ctrl-\ 1 focus tile 1 (session a) 325 # Ctrl-\ 1 focus tile 1 (the default session)
321 # type mark a again with zs-three 326 # type mark 0 again with zs-three
322 # Ctrl-\ d leave 327 # Ctrl-\ d leave
323 # 328 #
324 # Two claims: 329 # Two claims:
325 # 330 #
326 # * every marker landed in the session the focus was on, and in no 331 # * every marker landed in the session the focus was on, and in no
327 # other. zs-one and zs-three in a, zs-two in b, and each absent from 332 # other. zs-one and zs-three in `0`, zs-two in b, and each absent from
328 # the other session — the negatives are what prove the focus MOVED 333 # the other session — the negatives are what prove the focus MOVED
329 # rather than the wall typing on at one connection. 334 # rather than the wall typing on at one connection.
330 # * the daemon accepted exactly TWO attaches for the whole run — one per 335 # * the daemon accepted exactly TWO attaches for the whole run — one per
331 # tile, at wall startup. A `Ctrl-\ n` that re-dialled would be three or 336 # tile: the entry tile's, and the one the host's list grew for `b`. A
332 # more, and the cumulative counter says so however briefly the extra 337 # `Ctrl-\ n` that re-dialled would be three or more, and the cumulative
333 # connection lived. Focus is client-local and sends nothing on the wire. 338 # counter says so however briefly the extra connection lived. Focus is
339 # client-local and sends nothing on the wire.
334 start_daemon "$SOCK37" "$OUT.zs.d" "focus-skip daemon never bound" --shell /bin/sh 340 start_daemon "$SOCK37" "$OUT.zs.d" "focus-skip daemon never bound" --shell /bin/sh
335 D34PID=$DPID 341 D34PID=$DPID
336 342
337 # Both sessions exist before the wall does, each with a marker so the wall 343 # Both sessions exist before the wall does, each with a marker so the wall
338 # has something to paint and this leg has an anchor to enter on. 344 # has something to paint and this leg has an anchor to enter on. The first
339 pipe_mux "$OUT.zsa" "$OUT.zsa.err" timeout 40 "$MUX" --sock "$SOCK37" --session a 345 # of them is the daemon's OWN default session rather than a named one: a
346 # daemon is born holding `0`, so a wall over it always has that tile, and
347 # naming a second `a` would make this a three-tile leg pretending to be a
348 # two-tile one.
349 pipe_mux "$OUT.zsa" "$OUT.zsa.err" env XDG_STATE_HOME="$ZSSTATE" timeout 40 "$MUX" --sock "$SOCK37"
340 pipe_send 'printf "zsa-%%s\\n" pin\n' 350 pipe_send 'printf "zsa-%%s\\n" pin\n'
341 await_out "$OUT.zsa" "zsa-pin" "zsa-pin never reached the client" 351 await_out "$OUT.zsa" "zsa-pin" "zsa-pin never reached the client"
342 pipe_detach 352 pipe_detach
343 wait_grid "$SOCK37" "zsa-pin" "focus skip: session a's marker" a 353 wait_grid "$SOCK37" "zsa-pin" "focus skip: the default session's marker"
344 pipe_mux "$OUT.zsb" "$OUT.zsb.err" timeout 40 "$MUX" --sock "$SOCK37" --session b 354 pipe_mux "$OUT.zsb" "$OUT.zsb.err" env XDG_STATE_HOME="$ZSSTATE" timeout 40 "$MUX" --sock "$SOCK37" --session b
345 pipe_send 'printf "zsb-%%s\\n" pin\n' 355 pipe_send 'printf "zsb-%%s\\n" pin\n'
346 await_out "$OUT.zsb" "zsb-pin" "zsb-pin never reached the client" 356 await_out "$OUT.zsb" "zsb-pin" "zsb-pin never reached the client"
347 pipe_detach 357 pipe_detach
@@ -351,8 +361,13 @@ ZSATT_BEFORE=$(attaches_now "$SOCK37")
351 watch_clients "$SOCK37" "$OUT.zswatch" 361 watch_clients "$SOCK37" "$OUT.zswatch"
352 set +e 362 set +e
353 # tall: the aspect rule would cut columns at 92x30 (92 >= 60) 363 # tall: the aspect rule would cut columns at 92x30 (92 >= 60)
354 timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \ 364 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
355 "$MUX" wall "--sock $SOCK37#a" "--sock $SOCK37#b" > "$OUT.zspc" 2>&1 <<'EOF' 365 # came before it saved a sidecar on its way out. The aspect rule applies
366 # only when nothing is saved.
367 rm -f "$ZSSTATE/mux/layout"
368 XDG_STATE_HOME="$ZSSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \
369 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \
370 "$MUX" --sock "$SOCK37" > "$OUT.zspc" 2>&1 <<'EOF'
356 expect zsb-pin 20000 371 expect zsb-pin 20000
357 settle 700 20000 372 settle 700 20000
358 send printf 'zs-%s\\n' one\n 373 send printf 'zs-%s\\n' one\n
@@ -380,7 +395,7 @@ ZSATT_AFTER=$(attaches_now "$SOCK37")
380 cat "$OUT.zspc"; exit 1; } 395 cat "$OUT.zspc"; exit 1; }
381 # Where each marker landed. `muxa capture` reads the session's own grid, so 396 # Where each marker landed. `muxa capture` reads the session's own grid, so
382 # none of this can be an echo of what this script typed at a terminal. 397 # none of this can be an echo of what this script typed at a terminal.
383 timeout 20 "$MUXA" capture --sock "$SOCK37" --session a > "$OUT.zsfa" 2>&1 398 timeout 20 "$MUXA" capture --sock "$SOCK37" > "$OUT.zsfa" 2>&1
384 timeout 20 "$MUXA" capture --sock "$SOCK37" --session b > "$OUT.zsfb" 2>&1 399 timeout 20 "$MUXA" capture --sock "$SOCK37" --session b > "$OUT.zsfb" 2>&1
385 # 400 #
386 # The markers are numbered rather than named after their sessions, and 401 # The markers are numbered rather than named after their sessions, and
@@ -390,7 +405,7 @@ timeout 20 "$MUXA" capture --sock "$SOCK37" --session b > "$OUT.zsfb" 2>&1
390 # prefix, which is the property this needs. 405 # prefix, which is the property this needs.
391 for _m in zs-one zs-three; do 406 for _m in zs-one zs-three; do
392 grep -q "$_m" "$OUT.zsfa" || { 407 grep -q "$_m" "$OUT.zsfa" || {
393 echo "e2e FAIL: focus skip: session a never got $_m:" 408 echo "e2e FAIL: focus skip: the default session never got $_m:"
394 cat "$OUT.zsfa"; exit 1; } 409 cat "$OUT.zsfa"; exit 1; }
395 grep -q "$_m" "$OUT.zsfb" && { 410 grep -q "$_m" "$OUT.zsfb" && {
396 echo "e2e FAIL: focus skip: $_m reached session b, which the focus had left:" 411 echo "e2e FAIL: focus skip: $_m reached session b, which the focus had left:"
@@ -400,7 +415,7 @@ grep -q "zs-two" "$OUT.zsfb" || {
400 echo "e2e FAIL: focus skip: Ctrl-\\ n did not put the focus on session b:" 415 echo "e2e FAIL: focus skip: Ctrl-\\ n did not put the focus on session b:"
401 cat "$OUT.zsfb"; exit 1; } 416 cat "$OUT.zsfb"; exit 1; }
402 grep -q "zs-two" "$OUT.zsfa" && { 417 grep -q "zs-two" "$OUT.zsfa" && {
403 echo "e2e FAIL: focus skip: zs-two reached session a, so the focus never moved:" 418 echo "e2e FAIL: focus skip: zs-two reached the default session, so the focus never moved:"
404 cat "$OUT.zsfa"; exit 1; } 419 cat "$OUT.zsfa"; exit 1; }
405 # The whole point, said by the daemon: two tiles, two attaches, and never a 420 # The whole point, said by the daemon: two tiles, two attaches, and never a
406 # third across a focus move each way. This is the leg the counter matters 421 # third across a focus move each way. This is the leg the counter matters
@@ -408,12 +423,12 @@ grep -q "zs-two" "$OUT.zsfa" && {
408 # tell from a re-dial, since a connection that closes as another opens 423 # tell from a re-dial, since a connection that closes as another opens
409 # never shows two at once. 424 # never shows two at once.
410 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip" 425 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip"
411 assert_never_two_clients "$OUT.zswatch" a "focus skip" 426 assert_never_two_clients "$OUT.zswatch" 0 "focus skip"
412 assert_never_two_clients "$OUT.zswatch" b "focus skip" 427 assert_never_two_clients "$OUT.zswatch" b "focus skip"
413 # Both tiles claimed their rect: 40 cols each (this leg's own width) and 14 428 # Both tiles claimed their rect: 40 cols each (this leg's own width) and 14
414 # rows each (30 terminal rows cut into two stripes of 15, minus one label 429 # rows each (30 terminal rows cut into two stripes of 15, minus one label
415 # bar each). A focus move resizes nothing. 430 # bar each). A focus move resizes nothing.
416 for _s in a b; do 431 for _s in 0 b; do
417 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1 432 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1
418 grep -q '"cols":40' "$OUT.zsst$_s" || { 433 grep -q '"cols":40' "$OUT.zsst$_s" || {
419 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:" 434 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:"
@@ -462,8 +477,13 @@ until "$MUXD" dump --sock "$SOCK41" | grep -q "wln100"; do
462 sleep 0.1 477 sleep 0.1
463 done 478 done
464 set +e 479 set +e
465 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.zwcap" --err "$OUT.zwcap.err" -- \ 480 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
466 "$MUX" wall "--sock $SOCK41" > "$OUT.zwpc" 2>&1 <<'EOF' 481 # came before it saved a sidecar on its way out. The aspect rule applies
482 # only when nothing is saved.
483 rm -f "$ZWSTATE/mux/layout"
484 XDG_STATE_HOME="$ZWSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
485 --out "$OUT.zwcap" --err "$OUT.zwcap.err" -- \
486 "$MUX" --sock "$SOCK41" > "$OUT.zwpc" 2>&1 <<'EOF'
467 expect wln100 20000 487 expect wln100 20000
468 settle 700 20000 488 settle 700 20000
469 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M 489 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M
@@ -534,8 +554,13 @@ done
534 # not. The tile claims the terminal on its first pass, so the modes are 554 # not. The tile claims the terminal on its first pass, so the modes are
535 # level-set before the wheel is sent. 555 # level-set before the wheel is sent.
536 set +e 556 set +e
537 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.zm2cap" --err "$OUT.zm2cap.err" -- \ 557 # No saved tree: this leg asserts the DEFAULT cut, and every wall that
538 "$MUX" wall "--sock $SOCK42" > "$OUT.zm2pc" 2>&1 <<'EOF' 558 # came before it saved a sidecar on its way out. The aspect rule applies
559 # only when nothing is saved.
560 rm -f "$ZM2STATE/mux/layout"
561 XDG_STATE_HOME="$ZM2STATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
562 --out "$OUT.zm2cap" --err "$OUT.zm2cap.err" -- \
563 "$MUX" --sock "$SOCK42" > "$OUT.zm2pc" 2>&1 <<'EOF'
539 expect mapp-holds-the-mouse 20000 564 expect mapp-holds-the-mouse 20000
540 settle 700 20000 565 settle 700 20000
541 send \x1b[<64;10;5M 566 send \x1b[<64;10;5M
@@ -569,99 +594,3 @@ assert_stopped "$SOCK42" "$D39PID" "app mouse" "$OUT.zm2stop"
569 D39PID="" 594 D39PID=""
570 ok "an application in the focused tile gets the wheel, and the tile does not" 595 ok "an application in the focused tile gets the wheel, and the tile does not"
571 596
572 # ---- a tile whose pump has died still says something -------------------
573 #
574 # A dead tile keeps its rect and narrates on its label bar — the only
575 # paint a pump with nobody left to answer `repaint_gen` ever does. The
576 # keyboard is the only thread left, and it draws that bar so the wall
577 # never silently loses a row.
578 #
579 # The tile here is dead by the most ordinary route: a spelling naming a
580 # session that does not exist. The daemon refuses the attach and that pump
581 # ends — `[refused]`, the state the wall already had a word for. Its bar
582 # says so from the first paint, and the live tile beside it is unharmed:
583 # the wall is still a wall, and the focus stays on the tile that can be
584 # typed at.
585 #
586 # Deliberately NOT asserted: that a `[reconnecting]` tile gets the same
587 # treatment. It must not — it still has a thread that will repaint its hot
588 # replica within a poll timeout, and the keyboard drawing over that would
589 # replace something true with something stale.
590 start_daemon "$SOCK39" "$OUT.zd.d" "dead-tile daemon never bound" --shell /bin/sh
591 D36PID=$DPID
592 pipe_mux "$OUT.zda" "$OUT.zda.err" timeout 40 "$MUX" --sock "$SOCK39" --session a
593 pipe_send 'printf "zdlive-%%s\\n" pin\n'
594 await_out "$OUT.zda" "zdlive-pin" "zdlive-pin never reached the client"
595 pipe_detach
596 wait_grid "$SOCK39" "zdlive-pin" "dead tile: the live session's marker" a
597
598 set +e
599 # tall: two tiles at 90x24 trip the aspect rule (90 >= 48); 70x36 stays
600 # stacked (70 < 72) and fits the bar label the assertion greps for
601 timeout 90 "$PTYCLIENT" --cols 70 --rows 36 --out "$OUT.zdcap" --err "$OUT.zdcap.err" -- \
602 "$MUX" wall "--sock $SOCK39#a" "--sock $SOCK39#ghost" > "$OUT.zdpc" 2>&1 <<'EOF'
603 expect zdlive-pin 20000
604 settle 800 20000
605 send printf 'zdagain-%s\\n' pin\n
606 expect zdagain-pin 15000
607 settle 400 15000
608 send \x1c2
609 settle 400 15000
610 send \x1c:abc
611 settle 400 15000
612 send \x1b
613 settle 400 15000
614 send \x1c:zz#bad name
615 settle 400 15000
616 send \r
617 expect [bad target 10000
618 settle 600 15000
619 send \x1c1
620 settle 400 15000
621 send \x1cd
622 waitexit 10000
623 EOF
624 RC=$?
625 set -e
626 [ "$RC" -eq 0 ] || {
627 echo "e2e FAIL: dead tile: ptyclient leg exited $RC:"
628 cat "$OUT.zdpc"; exit 1; }
629 # The dead tile narrated its own refusal: the state word the bar can only
630 # have if the pump reached the refusal path. Painted from the first
631 # Painted from the first relayout, and never cleared.
632 grep -q -- "--sock $SOCK39#ghost \[refused\]" "$OUT.zdcap" || {
633 echo "e2e FAIL: dead tile: the dead tile's bar never said [refused]"; exit 1; }
634 # The live tile is unharmed and answering — the wall is still a wall.
635 grep -q -- "--sock $SOCK39#a \[up\]" "$OUT.zdcap" || {
636 echo "e2e FAIL: dead tile: the live tile's bar never said [up]"; exit 1; }
637 # The focus stayed on the live tile: the marker typed after the wall came
638 # up landed in a, which is the tile that can be typed at. A focus that
639 # landed on the dead one would have dropped these bytes.
640 timeout 20 "$MUXA" capture --sock "$SOCK39" --session a > "$OUT.zdcapa" 2>&1
641 grep -q "zdagain-pin" "$OUT.zdcapa" || {
642 echo "e2e FAIL: dead tile: the live session never got the focused marker:"
643 cat "$OUT.zdcapa"; exit 1; }
644 # `Ctrl-\ :` is the one chord that works with the focus on a dead tile —
645 # the splits go through the pump's `ask`, and a dead pump answers nothing —
646 # so it is the one chord that can leave a line on a row nothing repaints.
647 # The oracle, not the byte stream: a banner LEFT on the screen is in the
648 # capture either way, and only the render says what the human was still
649 # looking at.
650 "$RENDER" --cols 70 --rows 36 < "$OUT.zdcap" > "$OUT.zdgrid" || {
651 echo "e2e FAIL: dead tile: render oracle failed"
652 cat "$OUT.zdgrid"; exit 1; }
653 # The refusal reached the eyes that earned it. Nothing claims a dead tile,
654 # so a notice only the claim path can show would never have been seen.
655 grep -q "\[bad target" "$OUT.zdgrid" || {
656 echo "e2e FAIL: dead tile: a refused prompt never narrated on the dead tile:"
657 cat "$OUT.zdgrid"; exit 1; }
658 grep -q ": zz#bad" "$OUT.zdgrid" && {
659 echo "e2e FAIL: dead tile: the refused prompt is still on the dead tile's row:"
660 cat "$OUT.zdgrid"; exit 1; }
661 # And Esc leaves nothing behind either: same row, same missing claim.
662 grep -q ": abc" "$OUT.zdgrid" && {
663 echo "e2e FAIL: dead tile: the cancelled prompt is still on the dead tile's row:"
664 cat "$OUT.zdgrid"; exit 1; }
665 assert_stopped "$SOCK39" "$D36PID" "dead tile" "$OUT.zdstop"
666 D36PID=""
667 ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one"
test/e2e_11_select.sh
Old New
@@ -7,21 +7,37 @@
7 # out, so there is nothing left to reuse. 7 # out, so there is nothing left to reuse.
8 SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock" 8 SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock"
9 defer_sock "$SOCK50" 9 defer_sock "$SOCK50"
10 WMSTATE="${TMPDIR:-/tmp}/mux-e2e-wallmouse-state-$$"
11 defer_rm "$WMSTATE"
12 # The selection legs need a wall of ONE tile — a terminal row has to BE a
13 # grid row for a drag to be spelled by coordinate, and `assert_converged`
14 # diffs this client's whole screen against one daemon grid. A wall shows
15 # every session its daemon has, so "one tile" is now a statement about the
16 # DAEMON: this one is left holding nothing but the session it was born
17 # with.
18 SOCKSEL="${TMPDIR:-/tmp}/muxd-e2e-select-$$.sock"
19 defer_sock "$SOCKSEL"
20 SELSTATE="${TMPDIR:-/tmp}/mux-e2e-select-state-$$"
21 defer_rm "$SELSTATE"
10 22
11 # ---- the SOCK50 cluster: a daemon, two named sessions ------------------ 23 # ---- the SOCK50 cluster: a daemon, two sessions ------------------------
12 # 24 #
13 # A daemon and two named sessions (a, b) shared by the click leg below and 25 # A daemon and two sessions — its own default one and a named `b` —
14 # the drag legs after it. The wall-mouse filter this cluster used to open 26 # shared by the click legs below. A daemon is born holding `0`, so a wall
15 # with is gone — every click and drag routes to the focused tile's own 27 # over it always has that tile; naming a second would make these
16 # Core, hit-tested by rect — so what remains here is the setup: a daemon, 28 # two-tile legs three-tile ones. The wall-mouse filter this cluster used
17 # a marker per session, and the tiles a `mux wall` over them paints. 29 # to open with is gone — every click and drag routes to the focused tile's
30 # own Core, hit-tested by rect — so what remains here is the setup: a
31 # daemon, a marker per session, and the tiles `mux --sock` over it paints.
18 start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh 32 start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh
19 D48PID=$DPID 33 D48PID=$DPID
34 start_daemon "$SOCKSEL" "$OUT.sel.d" "selection daemon never bound" --shell /bin/sh
35 D49PID=$DPID
20 { printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ 36 { printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
21 timeout 40 "$MUX" --sock "$SOCK50" --session a > "$OUT.wmsa" 2> "$OUT.wmsa.err" 37 env XDG_STATE_HOME="$WMSTATE" timeout 40 "$MUX" --sock "$SOCK50" > "$OUT.wmsa" 2> "$OUT.wmsa.err"
22 wait_grid "$SOCK50" "wma-pin" "wall cluster: session a's marker" a 38 wait_grid "$SOCK50" "wma-pin" "wall cluster: the default session's marker"
23 { printf 'printf "wmb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ 39 { printf 'printf "wmb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
24 timeout 40 "$MUX" --sock "$SOCK50" --session b > "$OUT.wmsb" 2> "$OUT.wmsb.err" 40 env XDG_STATE_HOME="$WMSTATE" timeout 40 "$MUX" --sock "$SOCK50" --session b > "$OUT.wmsb" 2> "$OUT.wmsb.err"
25 wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b 41 wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b
26 42
27 # ...and a click in a tile's rect FOCUSES it. Two tiles over 30 rows is two 43 # ...and a click in a tile's rect FOCUSES it. Two tiles over 30 rows is two
@@ -40,8 +56,12 @@ wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b
40 # other way. A negative that cannot fail is not a test, so each carries the 56 # other way. A negative that cannot fail is not a test, so each carries the
41 # marker that MUST land in the focused session. 57 # marker that MUST land in the focused session.
42 set +e 58 set +e
43 timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \ 59 # No saved tree: this leg asserts the DEFAULT cut, and every wall before
44 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc3" 2>&1 <<'EOF' 60 # it saved a sidecar on its way out.
61 rm -f "$WMSTATE/mux/layout"
62 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
63 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \
64 "$MUX" --sock "$SOCK50" > "$OUT.wmpc3" 2>&1 <<'EOF'
45 expect wmb-pin 20000 65 expect wmb-pin 20000
46 settle 700 20000 66 settle 700 20000
47 send \x1b[<0;3;19M\x1b[<0;3;19m 67 send \x1b[<0;3;19M\x1b[<0;3;19m
@@ -57,7 +77,7 @@ set -e
57 [ "$RC" -eq 0 ] || { 77 [ "$RC" -eq 0 ] || {
58 echo "e2e FAIL: wall click: ptyclient leg exited $RC (did the click focus b?):" 78 echo "e2e FAIL: wall click: ptyclient leg exited $RC (did the click focus b?):"
59 cat "$OUT.wmpc3"; exit 1; } 79 cat "$OUT.wmpc3"; exit 1; }
60 timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 80 timeout 20 "$MUXA" capture --sock "$SOCK50" > "$OUT.wmfa" 2>&1
61 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 81 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
62 grep -q "wm-three" "$OUT.wmfb" || { 82 grep -q "wm-three" "$OUT.wmfb" || {
63 echo "e2e FAIL: wall click: a click in session b's stripe did not focus it:" 83 echo "e2e FAIL: wall click: a click in session b's stripe did not focus it:"
@@ -69,8 +89,12 @@ grep -q "wm-three" "$OUT.wmfa" && {
69 # The other direction: focus b explicitly, then click a's content and watch 89 # The other direction: focus b explicitly, then click a's content and watch
70 # the focus come back to a. 90 # the focus come back to a.
71 set +e 91 set +e
72 timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \ 92 # No saved tree: this leg asserts the DEFAULT cut, and every wall before
73 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc4" 2>&1 <<'EOF' 93 # it saved a sidecar on its way out.
94 rm -f "$WMSTATE/mux/layout"
95 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
96 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \
97 "$MUX" --sock "$SOCK50" > "$OUT.wmpc4" 2>&1 <<'EOF'
74 expect wmb-pin 20000 98 expect wmb-pin 20000
75 settle 700 20000 99 settle 700 20000
76 send \x1c2 100 send \x1c2
@@ -88,7 +112,7 @@ set -e
88 [ "$RC" -eq 0 ] || { 112 [ "$RC" -eq 0 ] || {
89 echo "e2e FAIL: wall click: label-bar leg exited $RC:" 113 echo "e2e FAIL: wall click: label-bar leg exited $RC:"
90 cat "$OUT.wmpc4"; exit 1; } 114 cat "$OUT.wmpc4"; exit 1; }
91 timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 115 timeout 20 "$MUXA" capture --sock "$SOCK50" > "$OUT.wmfa" 2>&1
92 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 116 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
93 grep -q "wm-four" "$OUT.wmfa" || { 117 grep -q "wm-four" "$OUT.wmfa" || {
94 echo "e2e FAIL: wall click: a click in session a's stripe did not focus it:" 118 echo "e2e FAIL: wall click: a click in session a's stripe did not focus it:"
@@ -104,8 +128,12 @@ ok "a click in a tile's rect focuses it, from wherever the focus was"
104 # test in rectHit earns its keep: focus starts on a (the left pane) and 128 # test in rectHit earns its keep: focus starts on a (the left pane) and
105 # the click must move it to b (the right pane). 129 # the click must move it to b (the right pane).
106 set +e 130 set +e
107 timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \ 131 # No saved tree: this leg asserts the DEFAULT cut, and every wall before
108 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc5" 2>&1 <<'EOF' 132 # it saved a sidecar on its way out.
133 rm -f "$WMSTATE/mux/layout"
134 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
135 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \
136 "$MUX" --sock "$SOCK50" > "$OUT.wmpc5" 2>&1 <<'EOF'
109 expect wmb-pin 20000 137 expect wmb-pin 20000
110 settle 700 20000 138 settle 700 20000
111 send \x1b[<0;70;9M\x1b[<0;70;9m 139 send \x1b[<0;70;9M\x1b[<0;70;9m
@@ -121,7 +149,7 @@ set -e
121 [ "$RC" -eq 0 ] || { 149 [ "$RC" -eq 0 ] || {
122 echo "e2e FAIL: wall click: beside leg exited $RC (did the column click focus b?):" 150 echo "e2e FAIL: wall click: beside leg exited $RC (did the column click focus b?):"
123 cat "$OUT.wmpc5"; exit 1; } 151 cat "$OUT.wmpc5"; exit 1; }
124 timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 152 timeout 20 "$MUXA" capture --sock "$SOCK50" > "$OUT.wmfa" 2>&1
125 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 153 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
126 grep -q "wm-five" "$OUT.wmfb" || { 154 grep -q "wm-five" "$OUT.wmfb" || {
127 echo "e2e FAIL: wall click: a click in the right pane did not focus it:" 155 echo "e2e FAIL: wall click: a click in the right pane did not focus it:"
@@ -139,10 +167,13 @@ ok "a click in a beside pane finds it by column, not just row"
139 # it lands on the CLIENT's tty as OSC 52, which the fixture holding that 167 # it lands on the CLIENT's tty as OSC 52, which the fixture holding that
140 # tty can read, so the assertion is the copied text itself. 168 # tty can read, so the assertion is the copied text itself.
141 # 169 #
142 # A one-tile wall, because that is where the geometry is knowable: `mux 170 # A one-tile wall, because that is where the geometry is knowable: a wall
143 # TARGET` is a wall of one tile whose rect is the whole terminal (no label 171 # over a daemon holding ONE session is one tile whose rect is the whole
144 # bar), so a terminal row IS a grid row and the text can be put on one by 172 # terminal (no label bar), so a terminal row IS a grid row and the text can
145 # name. The session is given its content with an explicit cursor address 173 # be put on one by name. Its own daemon for exactly that: the cluster's
174 # has two sessions and would cut two stripes.
175 #
176 # The session is given its content with an explicit cursor address
146 # rather than by printing lines, so the row this drag crosses does not 177 # rather than by printing lines, so the row this drag crosses does not
147 # depend on where a prompt happened to leave the cursor. 178 # depend on where a prompt happened to leave the cursor.
148 # 179 #
@@ -152,8 +183,9 @@ ok "a click in a beside pane finds it by column, not just row"
152 # Columns 1 to 9 and no further: the prompt lands at column 10 on the same 183 # Columns 1 to 9 and no further: the prompt lands at column 10 on the same
153 # row, and a selection that reached it would copy the prompt too. 184 # row, and a selection that reached it would copy the prompt too.
154 set +e 185 set +e
155 timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.selcap" --err "$OUT.selcap.err" -- \ 186 XDG_STATE_HOME="$SELSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
156 "$MUX" --sock "$SOCK50" --session s > "$OUT.selpc" 2>&1 <<'EOF' 187 --out "$OUT.selcap" --err "$OUT.selcap.err" -- \
188 "$MUX" --sock "$SOCKSEL" > "$OUT.selpc" 2>&1 <<'EOF'
157 settle 900 20000 189 settle 900 20000
158 send printf '\\033[2J\\033[9;1HZZ-%s' COPYME\n 190 send printf '\\033[2J\\033[9;1HZZ-%s' COPYME\n
159 expect ZZ-COPYME 15000 191 expect ZZ-COPYME 15000
@@ -192,10 +224,9 @@ SELCOPIES=$(grep -ao "$(printf '\033]52;')" "$OUT.selcap" | wc -l)
192 # `w漢字x-WIDEMARK`, so column 3 is the spacer tail of 漢 and column 4 the 224 # `w漢字x-WIDEMARK`, so column 3 is the spacer tail of 漢 and column 4 the
193 # first half of 字 — the two ways a span can cut a wide cell in half. 225 # first half of 字 — the two ways a span can cut a wide cell in half.
194 # 226 #
195 # The DEFAULT session, unlike the copy leg above: `converged_quiet` dumps 227 # The same one-session daemon the copy leg used: `converged_quiet` dumps
196 # the daemon's default grid, so a leg that named a session would diff this 228 # the daemon's default grid, so this client's screen has to be that grid
197 # client's screen against a grid it never attached to and fail for a reason 229 # and nothing else — one tile, no bar, no neighbour.
198 # that has nothing to do with wide cells.
199 # 230 #
200 # Asserted to fail, per the wan.sh rule: with `snapWide` stubbed out to 231 # Asserted to fail, per the wan.sh rule: with `snapWide` stubbed out to
201 # return its arguments, this leg renders `w 漢字x-WIDEMARK` against the 232 # return its arguments, this leg renders `w 漢字x-WIDEMARK` against the
@@ -209,8 +240,9 @@ SELCOPIES=$(grep -ao "$(printf '\033]52;')" "$OUT.selcap" | wc -l)
209 # highlight leaves nothing behind when it goes — the overlay-never-becomes- 240 # highlight leaves nothing behind when it goes — the overlay-never-becomes-
210 # state rule, on the only overlay that paints through the replica. 241 # state rule, on the only overlay that paints through the replica.
211 set +e 242 set +e
212 timeout 60 "$PTYCLIENT" --cols 40 --rows 12 --out "$OUT.swcap" --err "$OUT.swcap.err" -- \ 243 XDG_STATE_HOME="$SELSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 12 \
213 "$MUX" --sock "$SOCK50" > "$OUT.swpc" 2>&1 <<'EOF' 244 --out "$OUT.swcap" --err "$OUT.swcap.err" -- \
245 "$MUX" --sock "$SOCKSEL" > "$OUT.swpc" 2>&1 <<'EOF'
214 settle 900 20000 246 settle 900 20000
215 send printf '\\033[2J\\033[3;1Hw\\346\\274\\242\\345\\255\\227x-WIDEMARK'\n 247 send printf '\\033[2J\\033[3;1Hw\\346\\274\\242\\345\\255\\227x-WIDEMARK'\n
216 expect WIDEMARK 15000 248 expect WIDEMARK 15000
@@ -229,9 +261,11 @@ set -e
229 # The client's own stderr as well as the fixture's: the fixture can only 261 # The client's own stderr as well as the fixture's: the fixture can only
230 # report THAT the client closed the pty, and the reason is over here. 262 # report THAT the client closed the pty, and the reason is over here.
231 cat "$OUT.swpc" "$OUT.swcap.err"; exit 1; } 263 cat "$OUT.swpc" "$OUT.swcap.err"; exit 1; }
232 assert_converged "$OUT.swcap" "$SOCK50" "wide drag" 40 12 264 assert_converged "$OUT.swcap" "$SOCKSEL" "wide drag" 40 12
233 ok "a drag across wide cells leaves the client's screen converged" 265 ok "a drag across wide cells leaves the client's screen converged"
234 266
267 assert_stopped "$SOCKSEL" "$D49PID" "selection" "$OUT.selstop"
268 D49PID=""
235 assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop" 269 assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop"
236 D48PID="" 270 D48PID=""
237 ok "a drag copies on release, and a click copies nothing" 271 ok "a drag copies on release, and a click copies nothing"
@@ -254,10 +288,13 @@ defer_sock "$SOCK51"
254 # so the report — INPUT to the app — lands on the grid as text. Two reports 288 # so the report — INPUT to the app — lands on the grid as text. Two reports
255 # are asserted, because they are two code paths: the one the mode-set 289 # are asserted, because they are two code paths: the one the mode-set
256 # itself owes (the app sizes itself from it), and the one each resize owes. 290 # itself owes (the app sizes itself from it), and the one each resize owes.
291 IBSTATE="${TMPDIR:-/tmp}/mux-e2e-inband-state-$$"
292 defer_rm "$IBSTATE"
257 start_daemon "$SOCK51" "$OUT.inband.d" "in-band daemon never bound" --shell /bin/sh 293 start_daemon "$SOCK51" "$OUT.inband.d" "in-band daemon never bound" --shell /bin/sh
258 D51PID=$DPID 294 D51PID=$DPID
259 set +e 295 set +e
260 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.inband" --err "$OUT.inband.err" -- \ 296 XDG_STATE_HOME="$IBSTATE" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 \
297 --out "$OUT.inband" --err "$OUT.inband.err" -- \
261 "$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF' 298 "$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF'
262 expect \x1b[?1049h 15000 299 expect \x1b[?1049h 15000
263 settle 400 15000 300 settle 400 15000
@@ -326,6 +363,8 @@ ok "no XDG_RUNTIME_DIR: the default path is refused by name, --version is not"
326 # channel for the client that would not), and the SECOND is refused in 363 # channel for the client that would not), and the SECOND is refused in
327 # milliseconds (the offer was taken away, so nobody routes to the mute 364 # milliseconds (the offer was taken away, so nobody routes to the mute
328 # client again and ssh pays nothing). 365 # client again and ssh pays nothing).
366 AGSTATE="${TMPDIR:-/tmp}/mux-e2e-agtmute-state-$$"
367 defer_rm "$AGSTATE"
329 start_daemon "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound" --shell /bin/sh 368 start_daemon "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound" --shell /bin/sh
330 D42PID=$DPID 369 D42PID=$DPID
331 ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1 370 ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1
@@ -336,7 +375,7 @@ defer_kill "$AGENT48PID"
336 cat "$OUT.agtmute.env"; exit 1; } 375 cat "$OUT.agtmute.env"; exit 1; }
337 kill -STOP "$AGENT48PID" 376 kill -STOP "$AGENT48PID"
338 set +e 377 set +e
339 SSH_AUTH_SOCK="$AGENT48" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ 378 SSH_AUTH_SOCK="$AGENT48" XDG_STATE_HOME="$AGSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
340 --out "$OUT.agtmute" --err "$OUT.agtmute.err" \ 379 --out "$OUT.agtmute" --err "$OUT.agtmute.err" \
341 -- "$MUX" -A --sock "$SOCK48" > "$OUT.agtmute.log" 2>&1 <<'EOF' 380 -- "$MUX" -A --sock "$SOCK48" > "$OUT.agtmute.log" 2>&1 <<'EOF'
342 expect \x1b[?1049h 15000 381 expect \x1b[?1049h 15000
test/e2e_12_panes.sh
Old New
@@ -7,16 +7,28 @@
7 # reason every wall leg has. 7 # reason every wall leg has.
8 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" 8 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
9 defer_sock "$SOCK53" 9 defer_sock "$SOCK53"
10 SB2STATE="${TMPDIR:-/tmp}/mux-e2e-sbrect-state-$$"
11 defer_rm "$SB2STATE"
10 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock" 12 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock"
11 defer_sock "$SOCK54" 13 defer_sock "$SOCK54"
14 FSSTATE="${TMPDIR:-/tmp}/mux-e2e-fullscreen-state-$$"
15 defer_rm "$FSSTATE"
12 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock" 16 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock"
13 defer_sock "$SOCK55" 17 defer_sock "$SOCK55"
18 RSZSTATE="${TMPDIR:-/tmp}/mux-e2e-resize-state-$$"
19 defer_rm "$RSZSTATE"
14 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock" 20 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock"
15 defer_sock "$SOCK56" 21 defer_sock "$SOCK56"
22 SPCSTATE="${TMPDIR:-/tmp}/mux-e2e-spanclear-state-$$"
23 defer_rm "$SPCSTATE"
16 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock" 24 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock"
17 defer_sock "$SOCK57" 25 defer_sock "$SOCK57"
26 HJKSTATE="${TMPDIR:-/tmp}/mux-e2e-hjkl-state-$$"
27 defer_rm "$HJKSTATE"
18 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock" 28 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock"
19 defer_sock "$SOCK58" 29 defer_sock "$SOCK58"
30 SPLSTATE="${TMPDIR:-/tmp}/mux-e2e-splitbirth-state-$$"
31 defer_rm "$SPLSTATE"
20 SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock" 32 SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock"
21 defer_sock "$SOCK59" 33 defer_sock "$SOCK59"
22 SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock" 34 SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock"
@@ -30,6 +42,8 @@ defer_sock "$SOCK61"
30 # session another block created would change the tile it rests in. 42 # session another block created would change the tile it rests in.
31 SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock" 43 SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock"
32 defer_sock "$SOCK52" 44 defer_sock "$SOCK52"
45 CUSTATE="${TMPDIR:-/tmp}/mux-e2e-cursor-state-$$"
46 defer_rm "$CUSTATE"
33 # 47 #
34 # Every painter ends by showing the cursor where its own stripe sits, so 48 # Every painter ends by showing the cursor where its own stripe sits, so
35 # without an owner the visible cursor lands on whichever pump painted last: 49 # without an owner the visible cursor lands on whichever pump painted last:
@@ -40,33 +54,38 @@ defer_sock "$SOCK52"
40 # asserts the terminal's final cursor position is in tile 3's stripe — 54 # asserts the terminal's final cursor position is in tile 3's stripe —
41 # deterministic, because every paint now ends at the focused tile's cursor. 55 # deterministic, because every paint now ends at the focused tile's cursor.
42 # 56 #
43 # Three sessions on a daemon of its own, each marked by its shell before the 57 # Three sessions on a daemon of its own — the daemon's own `0` and two
44 # wall attaches (the shell-expanded marker trick: a hit is the shell's work, 58 # named ones, because a daemon is born holding `0` and a wall over it
59 # always has that tile — each marked by its shell before the wall attaches (the shell-expanded marker trick: a hit is the shell's work,
45 # never an echo of anything typed here). The ptyclient wall focuses tile 3, 60 # never an echo of anything typed here). The ptyclient wall focuses tile 3,
46 # settles, and detaches; the capture's last cursor-position escape is the 61 # settles, and detaches; the capture's last cursor-position escape is the
47 # witness. 62 # witness.
48 start_daemon "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound" --shell /bin/sh 63 start_daemon "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound" --shell /bin/sh
49 D52PID=$DPID 64 D52PID=$DPID
50 65
51 pipe_mux "$OUT.cua" "$OUT.cua.err" timeout 40 "$MUX" --sock "$SOCK52" --session a 66 pipe_mux "$OUT.cua" "$OUT.cua.err" env XDG_STATE_HOME="$CUSTATE" timeout 40 "$MUX" --sock "$SOCK52"
52 pipe_send 'printf "ma-%%s\\n" pin\n' 67 pipe_send 'printf "ma-%%s\\n" pin\n'
53 await_out "$OUT.cua" "ma-pin" "cursor: session a's marker never reached the client" 68 await_out "$OUT.cua" "ma-pin" "cursor: session a's marker never reached the client"
54 pipe_detach 69 pipe_detach
55 wait_grid "$SOCK52" "ma-pin" "cursor: session a's marker" a 70 wait_grid "$SOCK52" "ma-pin" "cursor: session a's marker"
56 pipe_mux "$OUT.cub" "$OUT.cub.err" timeout 40 "$MUX" --sock "$SOCK52" --session b 71 pipe_mux "$OUT.cub" "$OUT.cub.err" env XDG_STATE_HOME="$CUSTATE" timeout 40 "$MUX" --sock "$SOCK52" --session b
57 pipe_send 'printf "mb-%%s\\n" pin\n' 72 pipe_send 'printf "mb-%%s\\n" pin\n'
58 await_out "$OUT.cub" "mb-pin" "cursor: session b's marker never reached the client" 73 await_out "$OUT.cub" "mb-pin" "cursor: session b's marker never reached the client"
59 pipe_detach 74 pipe_detach
60 wait_grid "$SOCK52" "mb-pin" "cursor: session b's marker" b 75 wait_grid "$SOCK52" "mb-pin" "cursor: session b's marker" b
61 pipe_mux "$OUT.cuc" "$OUT.cuc.err" timeout 40 "$MUX" --sock "$SOCK52" --session c 76 pipe_mux "$OUT.cuc" "$OUT.cuc.err" env XDG_STATE_HOME="$CUSTATE" timeout 40 "$MUX" --sock "$SOCK52" --session c
62 pipe_send 'printf "mc-%%s\\n" pin\n' 77 pipe_send 'printf "mc-%%s\\n" pin\n'
63 await_out "$OUT.cuc" "mc-pin" "cursor: session c's marker never reached the client" 78 await_out "$OUT.cuc" "mc-pin" "cursor: session c's marker never reached the client"
64 pipe_detach 79 pipe_detach
65 wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c 80 wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c
66 81
67 set +e 82 set +e
68 timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \ 83 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
69 "$MUX" wall --sock "$SOCK52#a" "--sock $SOCK52#b" "--sock $SOCK52#c" > "$OUT.cupc" 2>&1 <<'EOF' 84 # above are walls too — each of them saved a one-leaf sidecar on its way
85 # out. The aspect rule applies only when nothing is saved.
86 rm -f "$CUSTATE/mux/layout"
87 XDG_STATE_HOME="$CUSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \
88 "$MUX" --sock "$SOCK52" > "$OUT.cupc" 2>&1 <<'EOF'
70 expect mc-pin 20000 89 expect mc-pin 20000
71 settle 500 15000 90 settle 500 15000
72 send \x1c3 91 send \x1c3
@@ -79,7 +98,19 @@ set -e
79 [ "$RC" -eq 0 ] || { 98 [ "$RC" -eq 0 ] || {
80 echo "e2e FAIL: cursor-ownership: ptyclient leg exited $RC:" 99 echo "e2e FAIL: cursor-ownership: ptyclient leg exited $RC:"
81 cat "$OUT.cupc" "$OUT.cucap.err"; exit 1; } 100 cat "$OUT.cupc" "$OUT.cucap.err"; exit 1; }
82 # Rows 21-30 are tile 3's stripe (3 tiles on 30 rows: stripes at 1/11/21). 101 # WHICH rows tile 3's stripe owns is read off the capture rather than
102 # counted: a tile's digit is fixed for its life, but tiles arrive from a
103 # host's list and are inserted beside the focus, so the third DIGIT is not
104 # necessarily the third stripe DOWN. Its own label bar's CUP row is the top
105 # of its stripe, and the nine rows under that are its content (30 rows over
106 # three tiles, one of each ten spent on the bar).
107 _bar3=$(grep -ao $'\x1b\\[[0-9][0-9]*;1H\x1b\\[7m 3> ' "$OUT.cucap" | tail -1)
108 _bar_row=${_bar3#??}
109 _bar_row=${_bar_row%%;*}
110 [ -n "$_bar_row" ] || {
111 echo "e2e FAIL: cursor-ownership: tile 3's focused label bar never painted,"
112 echo " so there is no stripe to hold the cursor to:"
113 cat "$OUT.cucap"; exit 1; }
83 # The focused tile's paint records its cursor; an unfocused paint's last act 114 # The focused tile's paint records its cursor; an unfocused paint's last act
84 # is a bare CUP putting it back there, with no show of its own. So the 115 # is a bare CUP putting it back there, with no show of its own. So the
85 # terminal's final cursor position — the last CUP in the capture, show or 116 # terminal's final cursor position — the last CUP in the capture, show or
@@ -91,8 +122,10 @@ _last_cup=$(grep -ao "$_cup_re" "$OUT.cucap" | tail -1)
91 # _last_cup is ESC[<row>;<col>H; peel the ESC[ prefix and the ;colH suffix. 122 # _last_cup is ESC[<row>;<col>H; peel the ESC[ prefix and the ;colH suffix.
92 _cur_row=${_last_cup#??} 123 _cur_row=${_last_cup#??}
93 _cur_row=${_cur_row%%;*} 124 _cur_row=${_cur_row%%;*}
94 [ -n "$_cur_row" ] && [ "$_cur_row" -ge 21 ] && [ "$_cur_row" -le 30 ] || { 125 [ -n "$_cur_row" ] && [ "$_cur_row" -gt "$_bar_row" ] && \
95 echo "e2e FAIL: cursor-ownership: the last CUP was '$_last_cup' (row ${_cur_row:-none}), want tile 3's stripe (rows 21-30):" 126 [ "$_cur_row" -le $((_bar_row + 9)) ] || {
127 echo "e2e FAIL: cursor-ownership: the last CUP was '$_last_cup' (row ${_cur_row:-none}),"
128 echo " want tile 3's stripe (rows $((_bar_row + 1))-$((_bar_row + 9))):"
96 cat "$OUT.cucap"; exit 1; } 129 cat "$OUT.cucap"; exit 1; }
97 assert_stopped "$SOCK52" "$D52PID" "cursor-ownership" "$OUT.custop" 130 assert_stopped "$SOCK52" "$D52PID" "cursor-ownership" "$OUT.custop"
98 D52PID="" 131 D52PID=""
@@ -121,24 +154,28 @@ D54PID=$DPID
121 # Session a: 100 lines of history, then cat. The wall attaches only once 154 # Session a: 100 lines of history, then cat. The wall attaches only once
122 # seq has finished, so the page the wheel fetches is content this tile was 155 # seq has finished, so the page the wheel fetches is content this tile was
123 # never sent live — the wheel leg's reason, verbatim. 156 # never sent live — the wheel leg's reason, verbatim.
124 pipe_mux "$OUT.sb2a" "$OUT.sb2a.err" timeout 40 "$MUX" --sock "$SOCK53" --session a 157 pipe_mux "$OUT.sb2a" "$OUT.sb2a.err" env XDG_STATE_HOME="$SB2STATE" timeout 40 "$MUX" --sock "$SOCK53"
125 pipe_send 'seq 1 100 | sed '\''s/^/wln/'\''; exec cat\n' 158 pipe_send 'seq 1 100 | sed '\''s/^/wln/'\''; exec cat\n'
126 await_out "$OUT.sb2a" "wln100" "scrollback-rect: session a never finished seq" 159 await_out "$OUT.sb2a" "wln100" "scrollback-rect: session a never finished seq"
127 pipe_detach 160 pipe_detach
128 wait_grid "$SOCK53" "wln100" "scrollback-rect: session a's tail" a 161 wait_grid "$SOCK53" "wln100" "scrollback-rect: session a's tail"
129 162
130 # Session b: a pin the capture can grep for, then cat. `printf "sb2%s\n" 163 # Session b: a pin the capture can grep for, then cat. `printf "sb2%s\n"
131 # nbr` so the pin is the output, not the echoed command — the focus-skip 164 # nbr` so the pin is the output, not the echoed command — the focus-skip
132 # leg's prefix trick. 165 # leg's prefix trick.
133 pipe_mux "$OUT.sb2b" "$OUT.sb2b.err" timeout 40 "$MUX" --sock "$SOCK53" --session b 166 pipe_mux "$OUT.sb2b" "$OUT.sb2b.err" env XDG_STATE_HOME="$SB2STATE" timeout 40 "$MUX" --sock "$SOCK53" --session b
134 pipe_send 'printf "sb2%%s\\n" nbr; exec cat\n' 167 pipe_send 'printf "sb2%%s\\n" nbr; exec cat\n'
135 await_out "$OUT.sb2b" "sb2nbr" "scrollback-rect: session b's pin never reached the client" 168 await_out "$OUT.sb2b" "sb2nbr" "scrollback-rect: session b's pin never reached the client"
136 pipe_detach 169 pipe_detach
137 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b 170 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b
138 171
139 set +e 172 set +e
140 timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \ 173 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
141 "$MUX" wall "--sock $SOCK53#a" "--sock $SOCK53#b" > "$OUT.sb2pc" 2>&1 <<'EOF' 174 # above are walls too — each of them saved a one-leaf sidecar on its way
175 # out. The aspect rule applies only when nothing is saved.
176 rm -f "$SB2STATE/mux/layout"
177 XDG_STATE_HOME="$SB2STATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \
178 "$MUX" --sock "$SOCK53" > "$OUT.sb2pc" 2>&1 <<'EOF'
142 expect sb2nbr 20000 179 expect sb2nbr 20000
143 settle 700 20000 180 settle 700 20000
144 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M 181 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M
@@ -182,12 +219,12 @@ ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's
182 start_daemon "$SOCK54" "$OUT.fs.d" "fullscreen daemon never bound" --shell /bin/sh 219 start_daemon "$SOCK54" "$OUT.fs.d" "fullscreen daemon never bound" --shell /bin/sh
183 D55PID=$DPID 220 D55PID=$DPID
184 221
185 pipe_mux "$OUT.fsa" "$OUT.fsa.err" timeout 40 "$MUX" --sock "$SOCK54" --session a 222 pipe_mux "$OUT.fsa" "$OUT.fsa.err" env XDG_STATE_HOME="$FSSTATE" timeout 40 "$MUX" --sock "$SOCK54"
186 pipe_send 'printf "fs-%%s\\n" init-a\n' 223 pipe_send 'printf "fs-%%s\\n" init-a\n'
187 await_out "$OUT.fsa" "fs-init-a" "fullscreen: session a's marker never reached the client" 224 await_out "$OUT.fsa" "fs-init-a" "fullscreen: session a's marker never reached the client"
188 pipe_detach 225 pipe_detach
189 wait_grid "$SOCK54" "fs-init-a" "fullscreen: session a's marker" a 226 wait_grid "$SOCK54" "fs-init-a" "fullscreen: session a's marker"
190 pipe_mux "$OUT.fsb" "$OUT.fsb.err" timeout 40 "$MUX" --sock "$SOCK54" --session b 227 pipe_mux "$OUT.fsb" "$OUT.fsb.err" env XDG_STATE_HOME="$FSSTATE" timeout 40 "$MUX" --sock "$SOCK54" --session b
191 pipe_send 'printf "fs-%%s\\n" init-b\n' 228 pipe_send 'printf "fs-%%s\\n" init-b\n'
192 await_out "$OUT.fsb" "fs-init-b" "fullscreen: session b's marker never reached the client" 229 await_out "$OUT.fsb" "fs-init-b" "fullscreen: session b's marker never reached the client"
193 pipe_detach 230 pipe_detach
@@ -195,8 +232,12 @@ wait_grid "$SOCK54" "fs-init-b" "fullscreen: session b's marker" b
195 232
196 set +e 233 set +e
197 # wide: 80x24 trips the aspect rule (80 >= 48 → beside) 234 # wide: 80x24 trips the aspect rule (80 >= 48 → beside)
198 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.fscap" --err "$OUT.fscap.err" -- \ 235 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
199 "$MUX" wall "--sock $SOCK54#a" "--sock $SOCK54#b" > "$OUT.fspc" 2>&1 <<'EOF' 236 # above are walls too — each of them saved a one-leaf sidecar on its way
237 # out. The aspect rule applies only when nothing is saved.
238 rm -f "$FSSTATE/mux/layout"
239 XDG_STATE_HOME="$FSSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.fscap" --err "$OUT.fscap.err" -- \
240 "$MUX" --sock "$SOCK54" > "$OUT.fspc" 2>&1 <<'EOF'
200 expect fs-init-b 20000 241 expect fs-init-b 20000
201 settle 700 20000 242 settle 700 20000
202 send \x1cf 243 send \x1cf
@@ -221,7 +262,7 @@ set -e
221 cat "$OUT.fspc" "$OUT.fscap.err"; exit 1; } 262 cat "$OUT.fspc" "$OUT.fscap.err"; exit 1; }
222 # fs-one landed in session a (the tile that was fullscreened first); 263 # fs-one landed in session a (the tile that was fullscreened first);
223 # fs-two landed in session b (the tile Ctrl-\ 2 moved the full rect to). 264 # fs-two landed in session b (the tile Ctrl-\ 2 moved the full rect to).
224 timeout 20 "$MUXA" capture --sock "$SOCK54" --session a > "$OUT.fsfa" 2>&1 265 timeout 20 "$MUXA" capture --sock "$SOCK54" > "$OUT.fsfa" 2>&1
225 timeout 20 "$MUXA" capture --sock "$SOCK54" --session b > "$OUT.fsfb" 2>&1 266 timeout 20 "$MUXA" capture --sock "$SOCK54" --session b > "$OUT.fsfb" 2>&1
226 grep -q "fs-one" "$OUT.fsfa" || { 267 grep -q "fs-one" "$OUT.fsfa" || {
227 echo "e2e FAIL: fullscreen: session a never got fs-one:" 268 echo "e2e FAIL: fullscreen: session a never got fs-one:"
@@ -267,12 +308,12 @@ ok "fullscreen gives the focused tile the terminal; focus follows; f restores"
267 start_daemon "$SOCK55" "$OUT.rsz.d" "resize daemon never bound" --shell /bin/sh 308 start_daemon "$SOCK55" "$OUT.rsz.d" "resize daemon never bound" --shell /bin/sh
268 D56PID=$DPID 309 D56PID=$DPID
269 310
270 pipe_mux "$OUT.rsza" "$OUT.rsza.err" timeout 40 "$MUX" --sock "$SOCK55" --session a 311 pipe_mux "$OUT.rsza" "$OUT.rsza.err" env XDG_STATE_HOME="$RSZSTATE" timeout 40 "$MUX" --sock "$SOCK55"
271 pipe_send 'printf "rsz-%%s\\n" init-a\n' 312 pipe_send 'printf "rsz-%%s\\n" init-a\n'
272 await_out "$OUT.rsza" "rsz-init-a" "resize: session a's marker never reached the client" 313 await_out "$OUT.rsza" "rsz-init-a" "resize: session a's marker never reached the client"
273 pipe_detach 314 pipe_detach
274 wait_grid "$SOCK55" "rsz-init-a" "resize: session a's marker" a 315 wait_grid "$SOCK55" "rsz-init-a" "resize: session a's marker"
275 pipe_mux "$OUT.rszb" "$OUT.rszb.err" timeout 40 "$MUX" --sock "$SOCK55" --session b 316 pipe_mux "$OUT.rszb" "$OUT.rszb.err" env XDG_STATE_HOME="$RSZSTATE" timeout 40 "$MUX" --sock "$SOCK55" --session b
276 pipe_send 'printf "rsz-%%s\\n" init-b\n' 317 pipe_send 'printf "rsz-%%s\\n" init-b\n'
277 await_out "$OUT.rszb" "rsz-init-b" "resize: session b's marker never reached the client" 318 await_out "$OUT.rszb" "rsz-init-b" "resize: session b's marker never reached the client"
278 pipe_detach 319 pipe_detach
@@ -282,8 +323,12 @@ set +e
282 # 80x24: aspect rule makes beside; two panes split 39/40 with the rail 323 # 80x24: aspect rule makes beside; two panes split 39/40 with the rail
283 # at column 40 (1-indexed). Three l presses grow the left pane by 3, 324 # at column 40 (1-indexed). Three l presses grow the left pane by 3,
284 # moving the rail to column 43. 325 # moving the rail to column 43.
285 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.rszcap" --err "$OUT.rszcap.err" -- \ 326 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
286 "$MUX" wall "--sock $SOCK55#a" "--sock $SOCK55#b" > "$OUT.rszpc" 2>&1 <<'EOF' 327 # above are walls too — each of them saved a one-leaf sidecar on its way
328 # out. The aspect rule applies only when nothing is saved.
329 rm -f "$RSZSTATE/mux/layout"
330 XDG_STATE_HOME="$RSZSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.rszcap" --err "$OUT.rszcap.err" -- \
331 "$MUX" --sock "$SOCK55" > "$OUT.rszpc" 2>&1 <<'EOF'
287 expect rsz-init-b 20000 332 expect rsz-init-b 20000
288 settle 700 20000 333 settle 700 20000
289 send \x1cr 334 send \x1cr
@@ -324,7 +369,7 @@ _last_rail=$(echo "$_rail_cols" | tail -1)
324 echo "e2e FAIL: resize: rail column is $_last_rail, want >=43 (40 + 3 grows):" 369 echo "e2e FAIL: resize: rail column is $_last_rail, want >=43 (40 + 3 grows):"
325 cat "$OUT.rszcap"; exit 1; } 370 cat "$OUT.rszcap"; exit 1; }
326 # Esc ended resize mode and typed prose reached the focused session. 371 # Esc ended resize mode and typed prose reached the focused session.
327 timeout 20 "$MUXA" capture --sock "$SOCK55" --session a > "$OUT.rszsta" 2>&1 372 timeout 20 "$MUXA" capture --sock "$SOCK55" > "$OUT.rszsta" 2>&1
328 timeout 20 "$MUXA" capture --sock "$SOCK55" --session b > "$OUT.rszstb" 2>&1 373 timeout 20 "$MUXA" capture --sock "$SOCK55" --session b > "$OUT.rszstb" 2>&1
329 if ! grep -q "rsz-grown" "$OUT.rszsta" && ! grep -q "rsz-grown" "$OUT.rszstb"; then 374 if ! grep -q "rsz-grown" "$OUT.rszsta" && ! grep -q "rsz-grown" "$OUT.rszstb"; then
330 echo "e2e FAIL: resize: rsz-grown reached neither session after Esc:" 375 echo "e2e FAIL: resize: rsz-grown reached neither session after Esc:"
@@ -346,12 +391,12 @@ ok "Ctrl-\\ r trades cells between panes; Esc returns to prose"
346 start_daemon "$SOCK56" "$OUT.spc.d" "span-clear daemon never bound" --shell /bin/sh 391 start_daemon "$SOCK56" "$OUT.spc.d" "span-clear daemon never bound" --shell /bin/sh
347 D57PID=$DPID 392 D57PID=$DPID
348 393
349 pipe_mux "$OUT.spca" "$OUT.spca.err" timeout 40 "$MUX" --sock "$SOCK56" --session a 394 pipe_mux "$OUT.spca" "$OUT.spca.err" env XDG_STATE_HOME="$SPCSTATE" timeout 40 "$MUX" --sock "$SOCK56"
350 pipe_send 'printf "spc-%%s\\n" left-flood\n' 395 pipe_send 'printf "spc-%%s\\n" left-flood\n'
351 await_out "$OUT.spca" "spc-left-flood" "span-clear: left session's marker never reached the client" 396 await_out "$OUT.spca" "spc-left-flood" "span-clear: left session's marker never reached the client"
352 pipe_detach 397 pipe_detach
353 wait_grid "$SOCK56" "spc-left-flood" "span-clear: left session's marker" a 398 wait_grid "$SOCK56" "spc-left-flood" "span-clear: left session's marker"
354 pipe_mux "$OUT.spcb" "$OUT.spcb.err" timeout 40 "$MUX" --sock "$SOCK56" --session b 399 pipe_mux "$OUT.spcb" "$OUT.spcb.err" env XDG_STATE_HOME="$SPCSTATE" timeout 40 "$MUX" --sock "$SOCK56" --session b
355 pipe_send 'printf "spc-%%s\\n" right-survivor\n' 400 pipe_send 'printf "spc-%%s\\n" right-survivor\n'
356 await_out "$OUT.spcb" "spc-right-survivor" "span-clear: right session's marker never reached the client" 401 await_out "$OUT.spcb" "spc-right-survivor" "span-clear: right session's marker never reached the client"
357 pipe_detach 402 pipe_detach
@@ -360,8 +405,12 @@ wait_grid "$SOCK56" "spc-right-survivor" "span-clear: right session's marker" b
360 set +e 405 set +e
361 # 80x24: aspect rule → beside. The wall client is a tty session: its 406 # 80x24: aspect rule → beside. The wall client is a tty session: its
362 # stdout is the terminal's full byte stream, captured by ptyclient. 407 # stdout is the terminal's full byte stream, captured by ptyclient.
363 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.spccap" --err "$OUT.spccap.err" -- \ 408 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
364 "$MUX" wall "--sock $SOCK56#a" "--sock $SOCK56#b" > "$OUT.spcpc" 2>&1 <<'EOF' 409 # above are walls too — each of them saved a one-leaf sidecar on its way
410 # out. The aspect rule applies only when nothing is saved.
411 rm -f "$SPCSTATE/mux/layout"
412 XDG_STATE_HOME="$SPCSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.spccap" --err "$OUT.spccap.err" -- \
413 "$MUX" --sock "$SOCK56" > "$OUT.spcpc" 2>&1 <<'EOF'
365 expect spc-right-survivor 20000 414 expect spc-right-survivor 20000
366 settle 700 20000 415 settle 700 20000
367 send \x1ch 416 send \x1ch
@@ -403,17 +452,21 @@ ok "a span-clear flood in the left pane cannot blank the right pane's grid"
403 start_daemon "$SOCK57" "$OUT.hjk.d" "hjkl daemon never bound" --shell /bin/sh 452 start_daemon "$SOCK57" "$OUT.hjk.d" "hjkl daemon never bound" --shell /bin/sh
404 D58PID=$DPID 453 D58PID=$DPID
405 454
406 pipe_mux "$OUT.hjkcap" "$OUT.hjkcap.err" timeout 40 "$MUX" --sock "$SOCK57" --session a 455 pipe_mux "$OUT.hjkcap" "$OUT.hjkcap.err" env XDG_STATE_HOME="$HJKSTATE" timeout 40 "$MUX" --sock "$SOCK57"
407 pipe_send 'printf "hjk-%%s\\n" origin\n' 456 pipe_send 'printf "hjk-%%s\\n" origin\n'
408 await_out "$OUT.hjkcap" "hjk-origin" "hjkl: origin marker never reached the client" 457 await_out "$OUT.hjkcap" "hjk-origin" "hjkl: origin marker never reached the client"
409 pipe_detach 458 pipe_detach
410 wait_grid "$SOCK57" "hjk-origin" "hjkl: origin marker" a 459 wait_grid "$SOCK57" "hjk-origin" "hjkl: origin marker"
411 460
412 set +e 461 set +e
413 # 80x24: aspect rule → beside for the first split; the second split goes 462 # 80x24: aspect rule → beside for the first split; the second split goes
414 # below, producing a 1 | (2 over 3) tree. 463 # below, producing a 1 | (2 over 3) tree.
415 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.hjkcap" --err "$OUT.hjkcap.err" -- \ 464 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
416 "$MUX" wall "--sock $SOCK57#a" > "$OUT.hjkpc" 2>&1 <<'EOF' 465 # above are walls too — each of them saved a one-leaf sidecar on its way
466 # out. The aspect rule applies only when nothing is saved.
467 rm -f "$HJKSTATE/mux/layout"
468 XDG_STATE_HOME="$HJKSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.hjkcap" --err "$OUT.hjkcap.err" -- \
469 "$MUX" --sock "$SOCK57" > "$OUT.hjkpc" 2>&1 <<'EOF'
417 expect hjk-origin 20000 470 expect hjk-origin 20000
418 settle 700 20000 471 settle 700 20000
419 send \x1c| 472 send \x1c|
@@ -445,7 +498,7 @@ set -e
445 cat "$OUT.hjkpc" "$OUT.hjkcap.err"; exit 1; } 498 cat "$OUT.hjkpc" "$OUT.hjkcap.err"; exit 1; }
446 # Three sessions exist now: a (the origin), and two birthed by splits. 499 # Three sessions exist now: a (the origin), and two birthed by splits.
447 # The daemon assigns numeric names to the new sessions; capture all three. 500 # The daemon assigns numeric names to the new sessions; capture all three.
448 timeout 20 "$MUXA" capture --sock "$SOCK57" --session a > "$OUT.hjkfa" 2>&1 501 timeout 20 "$MUXA" capture --sock "$SOCK57" > "$OUT.hjkfa" 2>&1
449 timeout 20 "$MUXA" capture --sock "$SOCK57" --session 1 > "$OUT.hjkfb" 2>&1 502 timeout 20 "$MUXA" capture --sock "$SOCK57" --session 1 > "$OUT.hjkfb" 2>&1
450 timeout 20 "$MUXA" capture --sock "$SOCK57" --session 2 > "$OUT.hjkfc" 2>&1 503 timeout 20 "$MUXA" capture --sock "$SOCK57" --session 2 > "$OUT.hjkfc" 2>&1
451 # went-left: typed after \x1ch from the right pane → lands in session a. 504 # went-left: typed after \x1ch from the right pane → lands in session a.
@@ -473,17 +526,21 @@ ok "hjkl walks focus across panes in both axes"
473 start_daemon "$SOCK58" "$OUT.spl.d" "split-birth daemon never bound" --shell /bin/sh 526 start_daemon "$SOCK58" "$OUT.spl.d" "split-birth daemon never bound" --shell /bin/sh
474 D59PID=$DPID 527 D59PID=$DPID
475 528
476 pipe_mux "$OUT.splcap" "$OUT.splcap.err" timeout 40 "$MUX" --sock "$SOCK58" --session a 529 pipe_mux "$OUT.splcap" "$OUT.splcap.err" env XDG_STATE_HOME="$SPLSTATE" timeout 40 "$MUX" --sock "$SOCK58"
477 pipe_send 'printf "spl-%%s\\n" origin\n' 530 pipe_send 'printf "spl-%%s\\n" origin\n'
478 await_out "$OUT.splcap" "spl-origin" "split-birth: origin marker never reached the client" 531 await_out "$OUT.splcap" "spl-origin" "split-birth: origin marker never reached the client"
479 pipe_detach 532 pipe_detach
480 wait_grid "$SOCK58" "spl-origin" "split-birth: origin marker" a 533 wait_grid "$SOCK58" "spl-origin" "split-birth: origin marker"
481 534
482 set +e 535 set +e
483 # 80x24: aspect rule → beside. One tile starts, then Ctrl-\ | births a 536 # 80x24: aspect rule → beside. One tile starts, then Ctrl-\ | births a
484 # second session to the right. 537 # second session to the right.
485 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.splcap" --err "$OUT.splcap.err" -- \ 538 # No saved tree: this leg asserts the DEFAULT cut, and the setup attaches
486 "$MUX" wall "--sock $SOCK58#a" > "$OUT.splpc" 2>&1 <<'EOF' 539 # above are walls too — each of them saved a one-leaf sidecar on its way
540 # out. The aspect rule applies only when nothing is saved.
541 rm -f "$SPLSTATE/mux/layout"
542 XDG_STATE_HOME="$SPLSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.splcap" --err "$OUT.splcap.err" -- \
543 "$MUX" --sock "$SOCK58" > "$OUT.splpc" 2>&1 <<'EOF'
487 expect spl-origin 20000 544 expect spl-origin 20000
488 settle 700 20000 545 settle 700 20000
489 send \x1c| 546 send \x1c|
@@ -496,9 +553,9 @@ set -e
496 [ "$RC" -eq 0 ] || { 553 [ "$RC" -eq 0 ] || {
497 echo "e2e FAIL: split-birth: ptyclient leg exited $RC:" 554 echo "e2e FAIL: split-birth: ptyclient leg exited $RC:"
498 cat "$OUT.splpc" "$OUT.splcap.err"; exit 1; } 555 cat "$OUT.splpc" "$OUT.splcap.err"; exit 1; }
499 # The daemon held sessions 0 (default) and a; the split birthed one 556 # The daemon held its own default session and nothing else; the split
500 # more, so three live sessions proves the chord created a new session. 557 # birthed one more, so two live sessions proves the chord created one.
501 wait_sessions "$SOCK58" 3 "split-birth: split did not birth a new session" 558 wait_sessions "$SOCK58" 2 "split-birth: split did not birth a new session"
502 # The rail paints as ESC[row;colH ESC[7m (reverse video). The new pane's 559 # The rail paints as ESC[row;colH ESC[7m (reverse video). The new pane's
503 # label bar is at a column greater than the rail's column — it sits to 560 # label bar is at a column greater than the rail's column — it sits to
504 # the right of the separator, which is the whole point of beside. 561 # the right of the separator, which is the whole point of beside.
@@ -523,42 +580,46 @@ ok "Ctrl-\\ | births a session beside; its label bar sits right of the rail"
523 580
524 # ---- layout sidecar: restore round-trip ------------------------------ 581 # ---- layout sidecar: restore round-trip ------------------------------
525 # 582 #
526 # A no-argv `mux wall` hydrates from the wall file, and on Ctrl-\ d the 583 # A no-argv `mux` opens the wall of hosts, and on Ctrl-\ d the resized tree
527 # resized tree is saved to the layout sidecar. Reattaching with the same 584 # is saved to the layout sidecar. Reattaching with the same state home
528 # state home loads the sidecar verbatim — the resize survived the round 585 # loads the sidecar verbatim — the resize survived the round trip. The rail's CUP column in the second capture is >= 43 (three
529 # trip. The rail's CUP column in the second capture is >= 43 (three
530 # right-resizes from a 40|40 split), and a marker typed into the restored 586 # right-resizes from a 40|40 split), and a marker typed into the restored
531 # wall lands in the focused session. 587 # wall lands in the focused session.
532 LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$" 588 LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$"
533 defer_rm "$LPSTATE" 589 defer_rm "$LPSTATE"
534 LPWALL="$LPSTATE/mux/wall" 590 LPHOSTS="$LPSTATE/mux/hosts"
535 LPLAYOUT="$LPSTATE/mux/layout" 591 LPLAYOUT="$LPSTATE/mux/layout"
536 start_daemon "$SOCK59" "$OUT.lpr.d" "layout-restore daemon never bound" --shell /bin/sh 592 start_daemon "$SOCK59" "$OUT.lpr.d" "layout-restore daemon never bound" --shell /bin/sh
537 D60PID=$DPID 593 D60PID=$DPID
538 594
539 pipe_mux "$OUT.lpra" "$OUT.lpra.err" timeout 40 "$MUX" --sock "$SOCK59" --session a 595 pipe_mux "$OUT.lpra" "$OUT.lpra.err" env XDG_STATE_HOME="$LPSTATE" timeout 40 "$MUX" --sock "$SOCK59"
540 pipe_send 'printf "lpr-%%s\\n" origin-a\n' 596 pipe_send 'printf "lpr-%%s\\n" origin-0\n'
541 await_out "$OUT.lpra" "lpr-origin-a" "layout-restore: session a marker never reached the client" 597 await_out "$OUT.lpra" "lpr-origin-0" "layout-restore: the default session's marker never reached the client"
542 pipe_detach 598 pipe_detach
543 wait_grid "$SOCK59" "lpr-origin-a" "layout-restore: session a marker" a 599 wait_grid "$SOCK59" "lpr-origin-0" "layout-restore: the default session's marker"
544 pipe_mux "$OUT.lprb" "$OUT.lprb.err" timeout 40 "$MUX" --sock "$SOCK59" --session b 600 pipe_mux "$OUT.lprb" "$OUT.lprb.err" env XDG_STATE_HOME="$LPSTATE" timeout 40 "$MUX" --sock "$SOCK59" --session b
545 pipe_send 'printf "lpr-%%s\\n" origin-b\n' 601 pipe_send 'printf "lpr-%%s\\n" origin-b\n'
546 await_out "$OUT.lprb" "lpr-origin-b" "layout-restore: session b marker never reached the client" 602 await_out "$OUT.lprb" "lpr-origin-b" "layout-restore: session b marker never reached the client"
547 pipe_detach 603 pipe_detach
548 wait_grid "$SOCK59" "lpr-origin-b" "layout-restore: session b marker" b 604 wait_grid "$SOCK59" "lpr-origin-b" "layout-restore: session b marker" b
549 605
550 # Write the wall file by hand: two spellings, one per line. The no-argv 606 # Write the hosts file by hand: ONE line, the daemon. Its two sessions are
551 # `mux wall` reads this on startup — the hydration path. 607 # what the poll turns into two panes — the wall knows nothing about them
608 # until it asks.
552 mkdir -p "$LPSTATE/mux" 609 mkdir -p "$LPSTATE/mux"
553 printf -- '--sock %s#a\n--sock %s#b\n' "$SOCK59" "$SOCK59" > "$LPWALL" 610 printf -- '--sock %s\n' "$SOCK59" > "$LPHOSTS"
611 # ...and no saved tree yet: the setup attaches above are walls too, and
612 # each saved a one-leaf sidecar on its way out. What this leg saves and
613 # restores has to be the tree the FIRST ptyclient builds.
614 rm -f "$LPLAYOUT"
554 615
555 set +e 616 set +e
556 # First ptyclient: hydrate, resize three right, detach. The detach saves 617 # First ptyclient: hydrate, resize three right, detach. The detach saves
557 # the sidecar. 618 # the sidecar.
558 XDG_STATE_HOME="$LPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 619 XDG_STATE_HOME="$LPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
559 --out "$OUT.lprcap1" --err "$OUT.lprcap1.err" -- \ 620 --out "$OUT.lprcap1" --err "$OUT.lprcap1.err" -- \
560 "$MUX" wall > "$OUT.lprpc1" 2>&1 <<'EOF' 621 "$MUX" > "$OUT.lprpc1" 2>&1 <<'EOF'
561 expect lpr-origin-a 20000 622 expect lpr-origin-0 20000
562 settle 700 20000 623 settle 700 20000
563 send \x1cr 624 send \x1cr
564 settle 300 5000 625 settle 300 5000
@@ -606,8 +667,8 @@ set +e
606 # the rail sits at column >= 43. Type a marker, then detach. 667 # the rail sits at column >= 43. Type a marker, then detach.
607 XDG_STATE_HOME="$LPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 668 XDG_STATE_HOME="$LPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
608 --out "$OUT.lprcap2" --err "$OUT.lprcap2.err" -- \ 669 --out "$OUT.lprcap2" --err "$OUT.lprcap2.err" -- \
609 "$MUX" wall > "$OUT.lprpc2" 2>&1 <<'EOF' 670 "$MUX" > "$OUT.lprpc2" 2>&1 <<'EOF'
610 expect lpr-origin-a 20000 671 expect lpr-origin-0 20000
611 settle 1000 20000 672 settle 1000 20000
612 send printf 'lpr-rt-%s\n' marker\n 673 send printf 'lpr-rt-%s\n' marker\n
613 expect lpr-rt-marker 10000 674 expect lpr-rt-marker 10000
@@ -638,55 +699,53 @@ timeout 20 "$MUXA" capture --sock "$SOCK59" --session b > "$OUT.lprfb" 2>&1
638 grep -q "lpr-rt-marker" "$OUT.lprfb" || { 699 grep -q "lpr-rt-marker" "$OUT.lprfb" || {
639 echo "e2e FAIL: layout-restore: marker not in session b (focus did not survive):" 700 echo "e2e FAIL: layout-restore: marker not in session b (focus did not survive):"
640 cat "$OUT.lprfb"; exit 1; } 701 cat "$OUT.lprfb"; exit 1; }
641 timeout 20 "$MUXA" capture --sock "$SOCK59" --session a > "$OUT.lprfa" 2>&1 702 timeout 20 "$MUXA" capture --sock "$SOCK59" > "$OUT.lprfa" 2>&1
642 ! grep -q "lpr-rt-marker" "$OUT.lprfa" || { 703 ! grep -q "lpr-rt-marker" "$OUT.lprfa" || {
643 echo "e2e FAIL: layout-restore: marker leaked into session a (focus was tile 0, not b):" 704 echo "e2e FAIL: layout-restore: marker leaked into the default session (focus was tile 0, not b):"
644 cat "$OUT.lprfa"; exit 1; } 705 cat "$OUT.lprfa"; exit 1; }
645 assert_stopped "$SOCK59" "$D60PID" "layout-restore" "$OUT.lprstop" 706 assert_stopped "$SOCK59" "$D60PID" "layout-restore" "$OUT.lprstop"
646 D60PID="" 707 D60PID=""
647 rm -rf "$LPSTATE" 708 rm -rf "$LPSTATE"
648 ok "a resized layout survives a detach/reattach round trip via the sidecar" 709 ok "a resized layout survives a detach/reattach round trip via the sidecar"
649 710
650 # ---- layout sidecar: healing on wall-file drift ---------------------- 711 # ---- layout sidecar: healing on LIVE drift ---------------------------
651 # 712 #
652 # After a 2-pane sidecar is saved, the wall file drifts: one original is 713 # The drift a wall of daemons can actually suffer, and it is no longer a
653 # removed and a new spelling is added. On reattach, restoreLayout heals — 714 # file being edited: a 2-pane sidecar is saved, and then one of those
654 # the survivor leaf keeps its session, the new tile inserts beside it, 715 # sessions ENDS while another is born. On reattach, restoreLayout heals —
655 # and the unmatched saved leaf collapses out. Two tiles, not three, and 716 # the survivor leaf keeps its session, the newcomer inserts beside it, and
656 # the wall did not refuse. 717 # the unmatched saved leaf collapses out. Two tiles, not three, and the
718 # wall did not refuse.
657 LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$" 719 LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$"
658 defer_rm "$LPHSTATE" 720 defer_rm "$LPHSTATE"
659 LPHWALL="$LPHSTATE/mux/wall" 721 LPHHOSTS="$LPHSTATE/mux/hosts"
660 LPHLAYOUT="$LPHSTATE/mux/layout" 722 LPHLAYOUT="$LPHSTATE/mux/layout"
661 start_daemon "$SOCK60" "$OUT.lph.d" "layout-heal daemon never bound" --shell /bin/sh 723 start_daemon "$SOCK60" "$OUT.lph.d" "layout-heal daemon never bound" --shell /bin/sh
662 D61PID=$DPID 724 D61PID=$DPID
663 725
664 pipe_mux "$OUT.lpha" "$OUT.lpha.err" timeout 40 "$MUX" --sock "$SOCK60" --session a 726 pipe_mux "$OUT.lpha" "$OUT.lpha.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60"
665 pipe_send 'printf "lph-%%s\\n" survivor-a\n' 727 pipe_send 'printf "lph-%%s\\n" survivor-0\n'
666 await_out "$OUT.lpha" "lph-survivor-a" "layout-heal: session a marker never reached the client" 728 await_out "$OUT.lpha" "lph-survivor-0" "layout-heal: the default session's marker never reached the client"
667 pipe_detach
668 wait_grid "$SOCK60" "lph-survivor-a" "layout-heal: session a marker" a
669 pipe_mux "$OUT.lphb" "$OUT.lphb.err" timeout 40 "$MUX" --sock "$SOCK60" --session b
670 pipe_send 'printf "lph-%%s\\n" removed-b\n'
671 await_out "$OUT.lphb" "lph-removed-b" "layout-heal: session b marker never reached the client"
672 pipe_detach 729 pipe_detach
673 wait_grid "$SOCK60" "lph-removed-b" "layout-heal: session b marker" b 730 wait_grid "$SOCK60" "lph-survivor-0" "layout-heal: the default session's marker"
674 pipe_mux "$OUT.lphc" "$OUT.lphc.err" timeout 40 "$MUX" --sock "$SOCK60" --session c 731 pipe_mux "$OUT.lphb" "$OUT.lphb.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session b
675 pipe_send 'printf "lph-%%s\\n" newcomer-c\n' 732 pipe_send 'printf "lph-%%s\\n" doomed-b\n'
676 await_out "$OUT.lphc" "lph-newcomer-c" "layout-heal: session c marker never reached the client" 733 await_out "$OUT.lphb" "lph-doomed-b" "layout-heal: session b marker never reached the client"
677 pipe_detach 734 pipe_detach
678 wait_grid "$SOCK60" "lph-newcomer-c" "layout-heal: session c marker" c 735 wait_grid "$SOCK60" "lph-doomed-b" "layout-heal: session b marker" b
679 736
680 # Wall file starts with a and b. The first ptyclient detaches and saves 737 # One line, the daemon. Its two sessions are the two panes the first
681 # a 2-pane sidecar for [a, b]. 738 # ptyclient saves a sidecar for.
682 mkdir -p "$LPHSTATE/mux" 739 mkdir -p "$LPHSTATE/mux"
683 printf -- '--sock %s#a\n--sock %s#b\n' "$SOCK60" "$SOCK60" > "$LPHWALL" 740 printf -- '--sock %s\n' "$SOCK60" > "$LPHHOSTS"
741 # No saved tree yet, for the restore leg's reason.
742 rm -f "$LPHLAYOUT"
684 743
685 set +e 744 set +e
686 XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 745 XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
687 --out "$OUT.lphcap1" --err "$OUT.lphcap1.err" -- \ 746 --out "$OUT.lphcap1" --err "$OUT.lphcap1.err" -- \
688 "$MUX" wall > "$OUT.lphpc1" 2>&1 <<'EOF' 747 "$MUX" > "$OUT.lphpc1" 2>&1 <<'EOF'
689 expect lph-survivor-a 20000 748 expect lph-survivor-0 20000
690 settle 700 20000 749 settle 700 20000
691 send \x1cd 750 send \x1cd
692 waitexit 10000 751 waitexit 10000
@@ -700,18 +759,29 @@ set -e
700 echo "e2e FAIL: layout-heal: sidecar not saved after first detach" 759 echo "e2e FAIL: layout-heal: sidecar not saved after first detach"
701 ls -la "$LPHSTATE/mux/" 2>&1; exit 1; } 760 ls -la "$LPHSTATE/mux/" 2>&1; exit 1; }
702 761
703 # Drift: remove session a's line, add session c's line. The sidecar still 762 # Drift, and the daemon is the one that does it: session b ENDS and
704 # describes [a, b]; the wall file now says [b, c]. On reattach, b is the 763 # session c is born. The sidecar still describes [0, b]; the wall the next
705 # survivor (matched), c inserts beside it, a collapses out (unmatched 764 # poll builds is [0, c]. On reattach, `0` is the survivor (matched), c
706 # saved leaf). 765 # inserts beside it, and b's saved leaf collapses out. Nothing was edited
707 printf -- '--sock %s#b\n--sock %s#c\n' "$SOCK60" "$SOCK60" > "$LPHWALL" 766 # — this is the only kind of drift a list of daemons can suffer.
767 pipe_mux "$OUT.lphb2" "$OUT.lphb2.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session b
768 pipe_send 'exit 0\n'
769 pipe_waitexit "layout-heal: session b's shell"
770 pipe_mux "$OUT.lphc" "$OUT.lphc.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session c
771 pipe_send 'printf "lph-%%s\\n" newcomer-c\n'
772 await_out "$OUT.lphc" "lph-newcomer-c" "layout-heal: session c marker never reached the client"
773 pipe_detach
774 wait_grid "$SOCK60" "lph-newcomer-c" "layout-heal: session c marker" c
775 wait_sessions "$SOCK60" 2 "layout-heal: the daemon should hold 0 and c"
708 776
709 set +e 777 set +e
710 XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 778 XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
711 --out "$OUT.lphcap2" --err "$OUT.lphcap2.err" -- \ 779 --out "$OUT.lphcap2" --err "$OUT.lphcap2.err" -- \
712 "$MUX" wall > "$OUT.lphpc2" 2>&1 <<'EOF' 780 "$MUX" > "$OUT.lphpc2" 2>&1 <<'EOF'
713 expect lph-removed-b 20000 781 expect lph-survivor-0 20000
714 settle 700 20000 782 settle 700 20000
783 send \x1c1
784 settle 500 15000
715 send printf 'lph-heal-%s\n' marker\n 785 send printf 'lph-heal-%s\n' marker\n
716 expect lph-heal-marker 10000 786 expect lph-heal-marker 10000
717 settle 500 15000 787 settle 500 15000
@@ -724,23 +794,31 @@ set -e
724 echo "e2e FAIL: layout-heal: second ptyclient exited $RC:" 794 echo "e2e FAIL: layout-heal: second ptyclient exited $RC:"
725 cat "$OUT.lphpc2" "$OUT.lphcap2.err"; exit 1; } 795 cat "$OUT.lphpc2" "$OUT.lphcap2.err"; exit 1; }
726 # The wall must not have refused: the ptyclient exited 0 (checked above), 796 # The wall must not have refused: the ptyclient exited 0 (checked above),
727 # and stderr mentions no layout error. The survivor (b) still reaches its 797 # and stderr mentions no layout error. The survivor still reaches its
728 # session — the marker typed after hydration landed in the focused tile, 798 # session — tile 1 is the leaf the sidecar kept, and a marker typed there
729 # which is tile 0 (session b, the first wall-file line). 799 # lands in the session that leaf names.
800 #
801 # The focus is CHOSEN rather than inherited, and deliberately: where a
802 # heal leaves the focus is the restore leg's claim, and that leg makes it
803 # with no drift in the way. Here the drift is the subject, so the leg
804 # names the tile it means.
730 grep -qi "layout" "$OUT.lphcap2.err" && { 805 grep -qi "layout" "$OUT.lphcap2.err" && {
731 echo "e2e FAIL: layout-heal: stderr mentions layout (heal was not silent):" 806 echo "e2e FAIL: layout-heal: stderr mentions layout (heal was not silent):"
732 cat "$OUT.lphcap2.err"; exit 1; } 807 cat "$OUT.lphcap2.err"; exit 1; }
733 # Two tiles present: the survivor (b) and the newcomer (c). The daemon 808 # Two tiles: the survivor and the newcomer. The daemon has 0 and c, and
734 # has sessions a, b, and c; the wall shows b and c. muxa capture of b 809 # the tile the sidecar's surviving leaf carries is the one the marker
735 # finds the survivor's marker. 810 # landed in.
736 timeout 20 "$MUXA" capture --sock "$SOCK60" --session b > "$OUT.lphfa" 2>&1 811 grep -q -- "--sock $SOCK60#c \[up\]" "$OUT.lphcap2" || {
812 echo "e2e FAIL: layout-heal: the newcomer never became a tile:"
813 cat "$OUT.lphpc2"; exit 1; }
814 timeout 20 "$MUXA" capture --sock "$SOCK60" > "$OUT.lphfa" 2>&1
737 grep -q "lph-heal-marker" "$OUT.lphfa" || { 815 grep -q "lph-heal-marker" "$OUT.lphfa" || {
738 echo "e2e FAIL: layout-heal: survivor's marker not in session b:" 816 echo "e2e FAIL: layout-heal: the survivor's marker is not in the session its leaf kept:"
739 cat "$OUT.lphfa"; exit 1; } 817 cat "$OUT.lphfa"; exit 1; }
740 assert_stopped "$SOCK60" "$D61PID" "layout-heal" "$OUT.lphstop" 818 assert_stopped "$SOCK60" "$D61PID" "layout-heal" "$OUT.lphstop"
741 D61PID="" 819 D61PID=""
742 rm -rf "$LPHSTATE" 820 rm -rf "$LPHSTATE"
743 ok "the layout heals on wall-file drift: survivor keeps its session, newcomer joins" 821 ok "the layout heals on live drift: the survivor keeps its session, the newcomer joins"
744 822
745 # ---- layout sidecar: silent degrade on garbage ----------------------- 823 # ---- layout sidecar: silent degrade on garbage -----------------------
746 # 824 #
@@ -751,19 +829,19 @@ ok "the layout heals on wall-file drift: survivor keeps its session, newcomer jo
751 # "layout" (the degrade is silent). 829 # "layout" (the degrade is silent).
752 LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$" 830 LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$"
753 defer_rm "$LPDSTATE" 831 defer_rm "$LPDSTATE"
754 LPDWALL="$LPDSTATE/mux/wall" 832 LPDHOSTS="$LPDSTATE/mux/hosts"
755 LPDLAYOUT="$LPDSTATE/mux/layout" 833 LPDLAYOUT="$LPDSTATE/mux/layout"
756 start_daemon "$SOCK61" "$OUT.lpd.d" "layout-degrade daemon never bound" --shell /bin/sh 834 start_daemon "$SOCK61" "$OUT.lpd.d" "layout-degrade daemon never bound" --shell /bin/sh
757 D62PID=$DPID 835 D62PID=$DPID
758 836
759 pipe_mux "$OUT.lpda" "$OUT.lpda.err" timeout 40 "$MUX" --sock "$SOCK61" --session a 837 pipe_mux "$OUT.lpda" "$OUT.lpda.err" env XDG_STATE_HOME="$LPDSTATE" timeout 40 "$MUX" --sock "$SOCK61"
760 pipe_send 'printf "lpd-%%s\\n" origin\n' 838 pipe_send 'printf "lpd-%%s\\n" origin\n'
761 await_out "$OUT.lpda" "lpd-origin" "layout-degrade: session a marker never reached the client" 839 await_out "$OUT.lpda" "lpd-origin" "layout-degrade: the default session's marker never reached the client"
762 pipe_detach 840 pipe_detach
763 wait_grid "$SOCK61" "lpd-origin" "layout-degrade: session a marker" a 841 wait_grid "$SOCK61" "lpd-origin" "layout-degrade: the default session's marker"
764 842
765 mkdir -p "$LPDSTATE/mux" 843 mkdir -p "$LPDSTATE/mux"
766 printf -- '--sock %s#a\n' "$SOCK61" > "$LPDWALL" 844 printf -- '--sock %s\n' "$SOCK61" > "$LPDHOSTS"
767 # A garbage sidecar: wrong magic, wrong version. restoreLayout returns 845 # A garbage sidecar: wrong magic, wrong version. restoreLayout returns
768 # null on the header check and the caller builds the default tree. 846 # null on the header check and the caller builds the default tree.
769 printf 'bogus 9\n' > "$LPDLAYOUT" 847 printf 'bogus 9\n' > "$LPDLAYOUT"
@@ -771,7 +849,7 @@ printf 'bogus 9\n' > "$LPDLAYOUT"
771 set +e 849 set +e
772 XDG_STATE_HOME="$LPDSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 850 XDG_STATE_HOME="$LPDSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
773 --out "$OUT.lpdcap" --err "$OUT.lpdcap.err" -- \ 851 --out "$OUT.lpdcap" --err "$OUT.lpdcap.err" -- \
774 "$MUX" wall > "$OUT.lpdpc" 2>&1 <<'EOF' 852 "$MUX" > "$OUT.lpdpc" 2>&1 <<'EOF'
775 expect lpd-origin 20000 853 expect lpd-origin 20000
776 settle 700 20000 854 settle 700 20000
777 send printf 'lpd-degrade-%s\n' marker\n 855 send printf 'lpd-degrade-%s\n' marker\n
@@ -786,7 +864,7 @@ set -e
786 echo "e2e FAIL: layout-degrade: ptyclient exited $RC:" 864 echo "e2e FAIL: layout-degrade: ptyclient exited $RC:"
787 cat "$OUT.lpdpc" "$OUT.lpdcap.err"; exit 1; } 865 cat "$OUT.lpdpc" "$OUT.lpdcap.err"; exit 1; }
788 # The wall came up: the marker reached the focused session. 866 # The wall came up: the marker reached the focused session.
789 timeout 20 "$MUXA" capture --sock "$SOCK61" --session a > "$OUT.lpdfa" 2>&1 867 timeout 20 "$MUXA" capture --sock "$SOCK61" > "$OUT.lpdfa" 2>&1
790 grep -q "lpd-degrade-marker" "$OUT.lpdfa" || { 868 grep -q "lpd-degrade-marker" "$OUT.lpdfa" || {
791 echo "e2e FAIL: layout-degrade: marker not in session a (wall did not come up):" 869 echo "e2e FAIL: layout-degrade: marker not in session a (wall did not come up):"
792 cat "$OUT.lpdfa"; exit 1; } 870 cat "$OUT.lpdfa"; exit 1; }
test/e2e_13_birth.sh
Old New
@@ -2,24 +2,6 @@
2 # e2e_13_birth.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in 2 # e2e_13_birth.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 3 # the order they stand in; see the lib's header for what this file may
4 # assume and what it must register. 4 # assume and what it must register.
5 # The hydrated-create leg. One daemon, and it serves QUIC as well as its
6 # socket: the leg's whole point is the LOCAL/remote split, so both halves
7 # have to be the same run of the same daemon or the comparison is between
8 # two rigs rather than between two kinds of line. Short socket name on
9 # purpose — the assertion reads a label bar back off a 70-column render.
10 SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock"
11 defer_sock "$SOCK64"
12 # Base 6000, below the 5000-spaced band the other QUIC ports share: every
13 # base from 11000 up is already taken, and a DUPLICATED base passes only
14 # for as long as the other leg's daemon happens to be dead by the time
15 # this one binds. A collision reads as this daemon's failure, not the
16 # suite's, which is why the base has to be the leg's own.
17 HYPORT=$(( 6000 + ($$ % 4000) ))
18 HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$"
19 defer_rm "$HYKEY"
20 HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$"
21 defer_rm "$HYSTATE"
22 HYWALL="$HYSTATE/mux/wall"
23 # The refused-attach leg: its own daemon, because its assertions are about 5 # The refused-attach leg: its own daemon, because its assertions are about
24 # what a daemon says to a name it does NOT have, and a socket shared with 6 # what a daemon says to a name it does NOT have, and a socket shared with
25 # another leg would let that leg's sessions decide what "does not have" 7 # another leg would let that leg's sessions decide what "does not have"
@@ -57,21 +39,22 @@ SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$"
57 defer_rm "$SPDIR" 39 defer_rm "$SPDIR"
58 SPINLOG="$SPDIR/dials" 40 SPINLOG="$SPDIR/dials"
59 41
60 # ---- Ctrl-\ : adds a tile by spelling --------------------------------- 42 # ---- Ctrl-\ : adds a HOST by spelling ---------------------------------
61 # 43 #
62 # Argv typed from inside. A spelling naming a session on ANOTHER daemon 44 # `mux hosts add` typed from inside. A spelling names a DAEMON: the line
63 # is born beside the focus, creates that session there, takes the focus, 45 # goes into the hosts file, a poller starts, and that daemon's sessions
64 # and is recorded into the wall file — mux TARGET's row of the birth 46 # become tiles — the prompt cannot conjure a session, only ask a machine
65 # table, without leaving the wall. A bad spelling is a notice and 47 # what it is running. A bad spelling is a notice and nothing else (the
66 # nothing else (the file does not grow). Esc eats the line: the shell 48 # file does not grow). Esc eats the line: the shell never sees it, and the
67 # never sees it, and the next keys reach the session again. 49 # next keys reach the session again.
68 # 50 #
69 # The prompt echoes what is typed, so the born tile's witness is not its 51 # The prompt echoes what is typed, so the added host's witness is not its
70 # label on the capture (the echo would match) but a marker typed AFTER 52 # label on the capture (the echo would match) but daemon B's OWN SHELL's
71 # the birth landing in daemon B's session b — a hit is B's shell's work. 53 # output painting on this wall — a marker B printed before this client
54 # existed, which only a real attach to B could put here.
72 PRSTATE="${TMPDIR:-/tmp}/mux-e2e-prompt-state-$$" 55 PRSTATE="${TMPDIR:-/tmp}/mux-e2e-prompt-state-$$"
73 defer_rm "$PRSTATE" 56 defer_rm "$PRSTATE"
74 PRWALL="$PRSTATE/mux/wall" 57 PRHOSTS="$PRSTATE/mux/hosts"
75 # Two daemons, because the assertion is that the birth landed on the one 58 # Two daemons, because the assertion is that the birth landed on the one
76 # the spelling NAMED: one socket could only ever say a session was created, 59 # the spelling NAMED: one socket could only ever say a session was created,
77 # not which daemon created it. 60 # not which daemon created it.
@@ -84,14 +67,25 @@ D63PID=$DPID
84 start_daemon "$SOCK63" "$OUT.prb.d" "prompt daemon B never bound" --shell /bin/sh 67 start_daemon "$SOCK63" "$OUT.prb.d" "prompt daemon B never bound" --shell /bin/sh
85 D64PID=$DPID 68 D64PID=$DPID
86 69
87 pipe_mux "$OUT.pra" "$OUT.pra.err" timeout 40 "$MUX" --sock "$SOCK62" --session a 70 pipe_mux "$OUT.pra" "$OUT.pra.err" env XDG_STATE_HOME="$PRSTATE" timeout 40 "$MUX" --sock "$SOCK62"
88 pipe_send 'printf "pr-%%s\\n" origin\n' 71 pipe_send 'printf "pr-%%s\\n" origin\n'
89 await_out "$OUT.pra" "pr-origin" "prompt: session a marker never reached the client" 72 await_out "$OUT.pra" "pr-origin" "prompt: daemon A's marker never reached the client"
90 pipe_detach 73 pipe_detach
91 wait_grid "$SOCK62" "pr-origin" "prompt: session a marker" a 74 wait_grid "$SOCK62" "pr-origin" "prompt: daemon A's marker"
75 # Daemon B's shell prints its own marker BEFORE this leg's wall exists.
76 # That is the witness: nothing on the wall can paint it unless the prompt
77 # really put B on the wall and B's session really became a tile.
78 pipe_mux "$OUT.prb" "$OUT.prb.err" env XDG_STATE_HOME="$PRSTATE" timeout 40 "$MUX" --sock "$SOCK63"
79 pipe_send 'printf "pr-%%s\\n" bmark\n'
80 await_out "$OUT.prb" "pr-bmark" "prompt: daemon B's marker never reached the client"
81 pipe_detach
82 wait_grid "$SOCK63" "pr-bmark" "prompt: daemon B's marker"
92 83
93 mkdir -p "$PRSTATE/mux" 84 mkdir -p "$PRSTATE/mux"
94 printf -- '--sock %s#a\n' "$SOCK62" > "$PRWALL" 85 printf -- '--sock %s\n' "$SOCK62" > "$PRHOSTS"
86 # No saved tree: the setup attaches are walls too, and each saved a
87 # one-leaf sidecar on its way out.
88 rm -f "$PRSTATE/mux/layout"
95 89
96 # The spelling goes in TWO sends with a settle between: the prompt paints 90 # The spelling goes in TWO sends with a settle between: the prompt paints
97 # its echo when a read ENDS with the prompt still open, so a spelling that 91 # its echo when a read ENDS with the prompt still open, so a spelling that
@@ -100,17 +94,16 @@ printf -- '--sock %s#a\n' "$SOCK62" > "$PRWALL"
100 set +e 94 set +e
101 XDG_STATE_HOME="$PRSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \ 95 XDG_STATE_HOME="$PRSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \
102 --out "$OUT.prcap" --err "$OUT.prcap.err" -- \ 96 --out "$OUT.prcap" --err "$OUT.prcap.err" -- \
103 "$MUX" wall > "$OUT.prpc" 2>&1 <<EOF 97 "$MUX" > "$OUT.prpc" 2>&1 <<EOF
104 expect pr-origin 20000 98 expect pr-origin 20000
105 settle 700 20000 99 settle 700 20000
106 send \x1c:--sock\x20 100 send \x1c:--sock\x20
107 settle 300 5000 101 settle 300 5000
108 send $SOCK63#b\r 102 send $SOCK63\r
103 expect pr-bmark 20000
109 settle 1000 20000 104 settle 1000 20000
110 send printf 'pr-born-%s\n' marker\n
111 expect pr-born-marker 15000
112 send \x1c:x#bad name\r 105 send \x1c:x#bad name\r
113 expect [bad target 10000 106 expect [bad host 10000
114 settle 500 15000 107 settle 500 15000
115 send \x1c:-A nosuchhost.invalid\r 108 send \x1c:-A nosuchhost.invalid\r
116 expect FlagLikeTarget 10000 109 expect FlagLikeTarget 10000
@@ -153,202 +146,43 @@ grep -q -- "-A nosuchhost.invalid \[" "$OUT.prgrid" && {
153 grep -aq ": --sock " "$OUT.prcap" || { 146 grep -aq ": --sock " "$OUT.prcap" || {
154 echo "e2e FAIL: prompt: the prompt line never painted its echo:" 147 echo "e2e FAIL: prompt: the prompt line never painted its echo:"
155 cat "$OUT.prpc"; exit 1; } 148 cat "$OUT.prpc"; exit 1; }
156 # Born on daemon B, focused: the marker typed after the birth is in B's 149 # Daemon B is really on this wall: its session got a stripe of its own,
157 # session b, and so are the keys typed after the Esc. 150 # naming B's socket, and the focus never left daemon A — the keys typed
158 timeout 20 "$MUXA" capture --sock "$SOCK63" --session b > "$OUT.prfb" 2>&1 151 # after the Esc are in A's session and in nothing of B's.
159 grep -q "pr-born-marker" "$OUT.prfb" || { 152 # B's own shell's output, on THIS wall's terminal. Not the bar: two
160 echo "e2e FAIL: prompt: marker not in daemon B's session b (tile not born there, or not focused):" 153 # stripes over 100 columns cut a socket path in half, and `labelText`
161 cat "$OUT.prfb"; exit 1; } 154 # spends what is left on the state word. The marker cannot be an echo —
162 grep -q "pr-after-esc" "$OUT.prfb" || { 155 # B printed it before this process existed.
163 echo "e2e FAIL: prompt: keys after Esc never reached session b (prompt did not close):" 156 grep -qa "pr-bmark" "$OUT.prcap" || {
164 cat "$OUT.prfb"; exit 1; } 157 echo "e2e FAIL: prompt: the added host's session never painted on the wall:"
165 grep -q "zzz" "$OUT.prfb" && { 158 cat "$OUT.prpc"; exit 1; }
159 timeout 20 "$MUXA" capture --sock "$SOCK62" > "$OUT.prfa" 2>&1
160 grep -q "pr-after-esc" "$OUT.prfa" || {
161 echo "e2e FAIL: prompt: keys after Esc never reached the focused session (prompt did not close):"
162 cat "$OUT.prfa"; exit 1; }
163 grep -q "zzz" "$OUT.prfa" && {
166 echo "e2e FAIL: prompt: Esc leaked the line into the shell:" 164 echo "e2e FAIL: prompt: Esc leaked the line into the shell:"
165 cat "$OUT.prfa"; exit 1; }
166 timeout 20 "$MUXA" capture --sock "$SOCK63" > "$OUT.prfb" 2>&1
167 grep -q "pr-after-esc" "$OUT.prfb" && {
168 echo "e2e FAIL: prompt: adding a host moved the focus onto it:"
167 cat "$OUT.prfb"; exit 1; } 169 cat "$OUT.prfb"; exit 1; }
168 # Recorded, and only the accepted spelling: two lines, the born one among them. 170 # Recorded, and only the accepted spelling: two lines, the added one among
169 grep -qF -- "--sock $SOCK63#b" "$PRWALL" || { 171 # them. The file is the wall the NEXT `mux` opens, so a host the user is
170 echo "e2e FAIL: prompt: the born tile was not recorded:" 172 # looking at and a host they get back are the same host.
171 cat "$PRWALL"; exit 1; } 173 grep -qxF -- "--sock $SOCK63" "$PRHOSTS" || {
172 [ "$(grep -c . "$PRWALL")" = "2" ] || { 174 echo "e2e FAIL: prompt: the added host was not recorded:"
173 echo "e2e FAIL: prompt: wall file is not exactly two lines (a refused spelling recorded?):" 175 cat "$PRHOSTS"; exit 1; }
174 cat "$PRWALL"; exit 1; } 176 [ "$(grep -c . "$PRHOSTS")" = "2" ] || {
177 echo "e2e FAIL: prompt: hosts file is not exactly two lines (a refused spelling recorded?):"
178 cat "$PRHOSTS"; exit 1; }
175 assert_stopped "$SOCK62" "$D63PID" "prompt A" "$OUT.prastop" 179 assert_stopped "$SOCK62" "$D63PID" "prompt A" "$OUT.prastop"
176 D63PID="" 180 D63PID=""
177 assert_stopped "$SOCK63" "$D64PID" "prompt B" "$OUT.prbstop" 181 assert_stopped "$SOCK63" "$D64PID" "prompt B" "$OUT.prbstop"
178 D64PID="" 182 D64PID=""
179 rm -rf "$PRSTATE" 183 rm -rf "$PRSTATE"
180 ok "Ctrl-\\ : adds a tile by spelling: born on another daemon, recorded, refusals narrated, Esc eats the line" 184 ok "Ctrl-\\ : adds a host by spelling: its sessions become tiles, recorded, refusals narrated, Esc eats the line"
181
182 # ---- a saved local line comes back as a fresh session -------------------
183 #
184 # The wall file is attach history, and the daemon that held those sessions
185 # dies on every reboot. Joining only meant the wall came back as a grid of
186 # [refused] tiles nobody could do anything with but forget them by hand.
187 # A restored line naming the LOCAL socket now attaches-or-creates.
188 #
189 # The remote half is the same wall, the same run and the same daemon,
190 # reached the other way: a `quic://` line for a session that does not
191 # exist stays [refused] and creates nothing. Recreating a shell on your
192 # own box is cheap and expected; spawning one on another host out of a
193 # saved file is not, and the split is the whole ruling.
194 #
195 # Daemon truth, not paint, answers "was it created": `muxa status` on a
196 # name the daemon does not have exits non-zero, which is the same probe
197 # the leg runs BEFORE the wall so it cannot pass vacuously. The render
198 # oracle answers the other question — what the human was looking at —
199 # because a bar left saying [refused] is invisible to a grep of the
200 # emitted bytes.
201 #
202 # Both roads a restored line can take are here, because they are two call
203 # sites of the one rule: the no-argv `mux wall` below runs the startup tile
204 # loop, and the `Ctrl-\ w` half at the end of this leg runs `hydrate()`.
205 head -c 32 /dev/urandom > "$HYKEY"
206 chmod 600 "$HYKEY"
207 start_daemon "$SOCK64" "$OUT.hyd.d" "hydrate-create daemon never bound" --shell /bin/sh \
208 --quic "127.0.0.1:$HYPORT" --key "$HYKEY" --quic-idle-ms 15000
209 D65PID=$DPID
210
211 # The wall file names three sessions this daemon does not have. Written by
212 # hand rather than earned by an attach, because an attach that earned the
213 # line would also have created the session — and then there would be
214 # nothing for the restore to create.
215 mkdir -p "$HYSTATE/mux"
216 printf -- '--sock %s#hya\n--sock %s#hyb\nquic://127.0.0.1:%s#hyghost\n' \
217 "$SOCK64" "$SOCK64" "$HYPORT" > "$HYWALL"
218
219 # Vacuity guard: none of the three exists yet. A leg whose daemon already
220 # had them would pass without the restore creating anything.
221 for _hy in hya hyb hyghost; do
222 timeout 20 "$MUXA" status --sock "$SOCK64" --session "$_hy" > "$OUT.hydpre" 2>&1 && {
223 echo "e2e FAIL: hydrate-create: session $_hy existed before the wall ran:"
224 cat "$OUT.hydpre"; exit 1; }
225 done
226
227 # 70x36: under the aspect rule (70 < 2*36) the cut is stacked, so each of
228 # the three tiles gets the full width and its label bar survives the
229 # render intact — the socket path is in that label.
230 set +e
231 XDG_STATE_HOME="$HYSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \
232 --out "$OUT.hydcap" --err "$OUT.hydcap.err" -- \
233 "$MUX" wall --key "$HYKEY" > "$OUT.hydpc" 2>&1 <<'EOF'
234 expect [up] 25000
235 settle 1000 25000
236 send printf 'hyd-%s\n' marker\n
237 expect hyd-marker 15000
238 settle 600 15000
239 send \x1cd
240 waitexit 10000
241 EOF
242 RC=$?
243 set -e
244 [ "$RC" -eq 0 ] || {
245 echo "e2e FAIL: hydrate-create: ptyclient exited $RC:"
246 cat "$OUT.hydpc" "$OUT.hydcap.err"; exit 1; }
247
248 # The local lines created their sessions, at the width of the stripe that
249 # created them. The width does not discriminate create from join on its own
250 # (a join's resize doorbell lands at 70 too, and the daemon refuses a create
251 # under min_session_cols) — the muxa exit code above is what says created.
252 # It is here to catch a session that came up at some OTHER client's size.
253 timeout 20 "$MUXA" status --sock "$SOCK64" --session hya > "$OUT.hydsta" 2>&1 || {
254 echo "e2e FAIL: hydrate-create: the saved line for hya did not create it:"
255 cat "$OUT.hydsta"; exit 1; }
256 grep -q '"cols":70' "$OUT.hydsta" || {
257 echo "e2e FAIL: hydrate-create: hya exists but not at the tile's width:"
258 cat "$OUT.hydsta"; exit 1; }
259 timeout 20 "$MUXA" status --sock "$SOCK64" --session hyb > "$OUT.hydstb" 2>&1 || {
260 echo "e2e FAIL: hydrate-create: the saved line for hyb did not create it:"
261 cat "$OUT.hydstb"; exit 1; }
262
263 # ...and the created session runs a real shell: the marker typed into the
264 # focused tile (tile 0, the first wall line) is the shell's own work.
265 timeout 20 "$MUXA" capture --sock "$SOCK64" --session hya > "$OUT.hydfa" 2>&1
266 grep -q "hyd-marker" "$OUT.hydfa" || {
267 echo "e2e FAIL: hydrate-create: the marker never reached the created session:"
268 cat "$OUT.hydfa"; exit 1; }
269
270 # The remote line created NOTHING. Same daemon, same run, same file — only
271 # the spelling differs.
272 timeout 20 "$MUXA" status --sock "$SOCK64" --session hyghost > "$OUT.hydghost" 2>&1 && {
273 echo "e2e FAIL: hydrate-create: a quic:// wall line created a session:"
274 cat "$OUT.hydghost"; exit 1; }
275
276 # What the human saw: two tiles up, the remote one still refused.
277 "$RENDER" --cols 70 --rows 36 < "$OUT.hydcap" > "$OUT.hydgrid" || {
278 echo "e2e FAIL: hydrate-create: render oracle failed"
279 cat "$OUT.hydgrid"; exit 1; }
280 grep -q -- "#hya \[up\]" "$OUT.hydgrid" || {
281 echo "e2e FAIL: hydrate-create: the hya tile never said [up]:"
282 cat "$OUT.hydgrid"; exit 1; }
283 grep -q -- "#hyb \[up\]" "$OUT.hydgrid" || {
284 echo "e2e FAIL: hydrate-create: the hyb tile never said [up]:"
285 cat "$OUT.hydgrid"; exit 1; }
286 grep -q -- "#hyghost \[refused\]" "$OUT.hydgrid" || {
287 echo "e2e FAIL: hydrate-create: the remote tile did not say [refused]:"
288 cat "$OUT.hydgrid"; exit 1; }
289 # ...and each bar carries the chord that reaches it. Ctrl-\ 1-9 focuses by
290 # POSITION while the bar named the session, and the two series disagree the
291 # moment a tile is forgotten or a session dies. Tile 1 has the focus here,
292 # so its digit carries the `>` and the other two the same-width blank.
293 grep -q -- " 1> .*#hya \[up\]" "$OUT.hydgrid" || {
294 echo "e2e FAIL: hydrate-create: the focused bar does not read \` 1> \`:"
295 cat "$OUT.hydgrid"; exit 1; }
296 grep -q -- " 2 .*#hyb \[up\]" "$OUT.hydgrid" || {
297 echo "e2e FAIL: hydrate-create: the second tile's bar does not read \` 2 \`:"
298 cat "$OUT.hydgrid"; exit 1; }
299 grep -q -- " 3 .*#hyghost \[refused\]" "$OUT.hydgrid" || {
300 echo "e2e FAIL: hydrate-create: a refused tile lost its chord digit:"
301 cat "$OUT.hydgrid"; exit 1; }
302 # The OTHER road a restored line takes: `Ctrl-\ w`. A no-argv `mux wall`
303 # runs the startup tile loop; the fold runs `hydrate()`, a different call
304 # site of the same rule, and the ruling covers both. Same daemon, same
305 # state home, one more ptyclient — a `mux TARGET` is the entry tile, and
306 # the fold brings the rest of the file in around it.
307 #
308 # The file is rewritten to name a session this daemon does not have. The
309 # entry attach then records its OWN line into the same file (attach
310 # history), which the fold dedupes by spelling — so what the fold has left
311 # to add is exactly the absent one, and creating it is the whole claim.
312 printf -- '--sock %s#hyfold\n' "$SOCK64" > "$HYWALL"
313 timeout 20 "$MUXA" status --sock "$SOCK64" --session hyfold > "$OUT.hydpre" 2>&1 && {
314 echo "e2e FAIL: hydrate-create: session hyfold existed before the fold ran:"
315 cat "$OUT.hydpre"; exit 1; }
316 185
317 set +e
318 XDG_STATE_HOME="$HYSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \
319 --out "$OUT.hydfcap" --err "$OUT.hydfcap.err" -- \
320 "$MUX" --sock "$SOCK64" --session hya > "$OUT.hydfpc" 2>&1 <<'EOF'
321 settle 1000 25000
322 send printf 'hydf-%s\n' pre\n
323 expect hydf-pre 20000
324 settle 600 20000
325 send \x1cw
326 settle 1500 25000
327 send \x1cd
328 waitexit 10000
329 EOF
330 RC=$?
331 set -e
332 [ "$RC" -eq 0 ] || {
333 echo "e2e FAIL: hydrate-create: fold ptyclient exited $RC:"
334 cat "$OUT.hydfpc" "$OUT.hydfcap.err"; exit 1; }
335
336 # Daemon truth again: the fold-born tile created the session its line named.
337 timeout 20 "$MUXA" status --sock "$SOCK64" --session hyfold > "$OUT.hydfold" 2>&1 || {
338 echo "e2e FAIL: hydrate-create: the folded line for hyfold did not create it:"
339 cat "$OUT.hydfold"; exit 1; }
340 # ...and it is a tile on the screen, not merely a session on the daemon.
341 "$RENDER" --cols 70 --rows 36 < "$OUT.hydfcap" > "$OUT.hydfgrid" || {
342 echo "e2e FAIL: hydrate-create: fold render oracle failed"
343 cat "$OUT.hydfgrid"; exit 1; }
344 grep -q -- "#hyfold \[up\]" "$OUT.hydfgrid" || {
345 echo "e2e FAIL: hydrate-create: the fold-born tile never said [up]:"
346 cat "$OUT.hydfgrid"; exit 1; }
347
348 assert_stopped "$SOCK64" "$D65PID" "hydrate-create" "$OUT.hydstop"
349 D65PID=""
350 rm -rf "$HYSTATE"
351 ok "a saved local line comes back as a fresh session, by wall and by fold; a remote one joins only"
352 186
353 # --------------------------------------------------------------------------- 187 # ---------------------------------------------------------------------------
354 # A refused attach is not a shell that exited. 188 # A refused attach is not a shell that exited.