a73x

13e66d44

test: the e2e suite pins the wall as the layout

a73x   2026-09-03 05:20

Commit message
test: the e2e suite pins the wall as the layout

Every leg that used to get its tiles from a daemon's list now AUTHORS them:
`seed_layout` writes the `mux-layout 1` file a `mux` opens on, because the
poll grades panes and adds none. Eleven legs are rewritten to the new
claims — a wall shows its layout's panes and no more, `Ctrl-\ x` removes a
pane and ends nothing, the picker's `x` is what ends a session, the hub
serves the layout's leaves and `+` writes a birth back into it — and one is
new: two state homes over the same two daemons are two walls, and neither
learns of the other's panes. The pin goes 109 -> 110.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

test/e2e.sh
Old New
@@ -170,8 +170,8 @@ done
170 # one of those and adds a convergence point would be pinning a fact every 170 # one of those and adds a convergence point would be pinning a fact every
171 # leg above already establishes. 171 # leg above already establishes.
172 172
173 [ "$OK_COUNT" = "109" ] || { 173 [ "$OK_COUNT" = "110" ] || {
174 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 109 —" 174 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 110 —"
175 echo " a scenario was added (update the pin) or silently lost" 175 echo " a scenario was added (update the pin) or silently lost"
176 exit 1 176 exit 1
177 } 177 }
test/e2e_05_session.sh
Old New
@@ -229,17 +229,21 @@ wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b
229 # Its own state home: `mux web --sock S` RECORDS S in the hosts file it 229 # Its own state home: `mux web --sock S` RECORDS S in the hosts file it
230 # then serves, and every other leg's daemons are in the shared one. 230 # then serves, and every other leg's daemons are in the shared one.
231 hostroom m18wall 231 hostroom m18wall
232 # The hub serves the LAYOUT's panes, so the three tiles are three leaves.
233 # The claim is unchanged — one label, three sessions beside it — and what
234 # writes them down is no longer a poll.
235 seed_layout "$HOSTROOM" stacked "--sock $SOCK22#0" "--sock $SOCK22#a" "--sock $SOCK22#b"
232 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK22" --port "$WPORT3" > "$OUT.m18wh" 2>&1 & 236 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK22" --port "$WPORT3" > "$OUT.m18wh" 2>&1 &
233 W3PID=$! 237 W3PID=$!
234 defer_kill "$W3PID" 238 defer_kill "$W3PID"
235 M18ORIG="http://127.0.0.1:$WPORT3" 239 M18ORIG="http://127.0.0.1:$WPORT3"
236 # Three tiles: the daemon's own default session and the two above. `tile 2:` 240 # Three tiles, one per leaf. `tile 2:` is the last of them, so waiting on
237 # is the last of them, so waiting on it is waiting for the whole list. 241 # it is waiting for the whole wall.
238 wait_for "$OUT.m18wh" "tile 2:" 15 || { 242 wait_for "$OUT.m18wh" "tile 2:" 15 || {
239 echo "e2e FAIL: M18 wall hub never announced its tiles"; cat "$OUT.m18wh"; exit 1; } 243 echo "e2e FAIL: M18 wall hub never announced its tiles"; cat "$OUT.m18wh"; exit 1; }
240 244
241 # ONE label, three sessions beside it — which is the milestone's whole 245 # ONE label, three sessions beside it, in the file's own leaf order —
242 # claim, read off the page's own answer. 246 # which is the milestone's whole claim, read off the page's own answer.
243 M18WANT="[{\"label\":\"--sock $SOCK22\",\"session\":\"0\"}," 247 M18WANT="[{\"label\":\"--sock $SOCK22\",\"session\":\"0\"},"
244 M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"a\"}," 248 M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"a\"},"
245 M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"b\"}]" 249 M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"b\"}]"
test/e2e_06_web.sh
Old New
@@ -113,12 +113,15 @@ D15PID=$DPID
113 # it then serves, so a shared home would give this hub every other leg's 113 # it then serves, so a shared home would give this hub every other leg's
114 # daemons as tiles too. 114 # daemons as tiles too.
115 hostroom webb 115 hostroom webb
116 # ...and a layout of its own: the hub serves the LAYOUT's panes, so a hosts
117 # file alone is a hub with nothing on it. One leaf, the daemon's own
118 # session 0, which is the tile the stand-in below dials.
119 seed_layout "$HOSTROOM" stacked "--sock $SOCK19#0"
116 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK19" --port "$WPORT" > "$OUT.webh" 2>&1 & 120 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK19" --port "$WPORT" > "$OUT.webh" 2>&1 &
117 W1PID=$! 121 W1PID=$!
118 defer_kill "$W1PID" 122 defer_kill "$W1PID"
119 # `serving` is the door; a TILE is a live session on a listed daemon, which 123 # `serving` is the door; a TILE is a leaf of the layout, announced as the
120 # the hub does not know until its first poll comes back. The stand-in dials 124 # hub seats it. The stand-in dials `/ws/0`, so wait for the id to exist.
121 # `/ws/0`, so wait for the id to exist.
122 wait_for "$OUT.webh" "tile 0:" 10 || { 125 wait_for "$OUT.webh" "tile 0:" 10 || {
123 echo "e2e FAIL: hub never announced a tile"; cat "$OUT.webh"; exit 1; } 126 echo "e2e FAIL: hub never announced a tile"; cat "$OUT.webh"; exit 1; }
124 127
@@ -184,6 +187,7 @@ ok "hub pumps a real session; wrong origin refused"
184 start_daemon "$SOCK20" "$OUT.webc.d" "web tear daemon never bound" --shell /bin/sh 187 start_daemon "$SOCK20" "$OUT.webc.d" "web tear daemon never bound" --shell /bin/sh
185 D16PID=$DPID 188 D16PID=$DPID
186 hostroom webc 189 hostroom webc
190 seed_layout "$HOSTROOM" stacked "--sock $SOCK20#0"
187 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK20" --port "$WPORT2" > "$OUT.webh2" 2>&1 & 191 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK20" --port "$WPORT2" > "$OUT.webh2" 2>&1 &
188 W2PID=$! 192 W2PID=$!
189 defer_kill "$W2PID" 193 defer_kill "$W2PID"
@@ -245,16 +249,20 @@ assert_stopped "$SOCK20" "$D17PID" "web tear: the restarted daemon" "$OUT.websto
245 D17PID="" 249 D17PID=""
246 ok "hub narrates the tear; the replica re-attaches across an epoch" 250 ok "hub narrates the tear; the replica re-attaches across an epoch"
247 251
248 # --- M-wall: the hub's wall is the HOSTS FILE, and its tiles are those 252 # --- M-wall: the hub's wall is the LAYOUT. `/tiles` lists its leaves in
249 # daemons' live sessions. Nothing here is authored in the browser: the 253 # the file's own order, a session born anywhere else is not on it, and `+`
250 # page shows what the daemons have, `+` births one, and the routes that 254 # writes the pane it births back into the file. Nothing here is authored
251 # used to add, remove and reorder answer 405. Every mux web here runs with 255 # in the browser beyond that one verb: the routes that used to add, remove
252 # a state home of its own (see $DWSTATE), so the file this leg reads back 256 # and reorder answer 405. Every mux web here runs with a state home of its
253 # is the file this leg wrote — and never the developer's real one. 257 # own (see $DWSTATE), so the file this leg reads back is the file this leg
258 # wrote — and never the developer's real one.
254 # 259 #
255 # TWO daemons and an off-origin session on the first: a wall of one host 260 # TWO daemons and an off-origin session on the first, and the layout puts
256 # is blind to host order, and a leg whose only session is `0` is blind to 261 # them in an order NO other rule would produce — B's session before A's
257 # which session a tile names. 262 # two, and A's `b` before A's `0`. A wall in host order then daemon order
263 # (which is what the hub used to build) reads `A#0 A#b B#0` here, so the
264 # order assertion below is a claim about the FILE and not a restatement of
265 # how the fixture happens to be built.
258 start_daemon "$SOCK25" "$OUT.dwa.d" "host wall daemon A never bound" --shell /bin/sh 266 start_daemon "$SOCK25" "$OUT.dwa.d" "host wall daemon A never bound" --shell /bin/sh
259 D22PID=$DPID 267 D22PID=$DPID
260 start_daemon "$SOCK76" "$OUT.dwb.d" "host wall daemon B never bound" --shell /bin/sh 268 start_daemon "$SOCK76" "$OUT.dwb.d" "host wall daemon B never bound" --shell /bin/sh
@@ -294,6 +302,11 @@ softkill "$W5PID" || true
294 wait_pid_gone "$W5PID" "host wall: empty-file hub killed by tracked pid" 302 wait_pid_gone "$W5PID" "host wall: empty-file hub killed by tracked pid"
295 W5PID="" 303 W5PID=""
296 304
305 # The wall, authored: three leaves over the two daemons, in an order
306 # neither host order nor list order would give.
307 seed_layout "$DWSTATE" beside \
308 "--sock $SOCK76#0" "--sock $SOCK25#b" "--sock $SOCK25#0"
309
297 # Argv is RECORDED, not a view of its own: `mux web "--sock A" "--sock B"` 310 # Argv is RECORDED, not a view of its own: `mux web "--sock A" "--sock B"`
298 # is `mux hosts add` twice and then a hub on the file. 311 # is `mux hosts add` twice and then a hub on the file.
299 XDG_STATE_HOME="$DWSTATE" "$MUX" web "--sock $SOCK25" "--sock $SOCK76" --port "$WPORT4" > "$OUT.dwh" 2>&1 & 312 XDG_STATE_HOME="$DWSTATE" "$MUX" web "--sock $SOCK25" "--sock $SOCK76" --port "$WPORT4" > "$OUT.dwh" 2>&1 &
@@ -316,11 +329,10 @@ grep -qxF -- "--sock $SOCK76" "$DWSTATE/mux/hosts" || {
316 echo "e2e FAIL: host wall: the hub wrote a wall file; it holds:" 329 echo "e2e FAIL: host wall: the hub wrote a wall file; it holds:"
317 cat "$DWSTATE/mux/wall"; exit 1; } 330 cat "$DWSTATE/mux/wall"; exit 1; }
318 331
319 # Three tiles, announced by the pollers as their hosts answer: A's `0` and 332 # Three tiles, one per leaf, announced as the hub seats them. Ids are
320 # `b`, and B's `0`. Ids are BIRTH order and one poller thread per host 333 # BIRTH order and a `+` mints more of them, so the ids are not this leg's
321 # races the other, so the ids are not this leg's to predict — the wall's 334 # to predict — the wall's ORDER is the FILE's, and `/ws/<id>` is read out
322 # ORDER is (host order, then the daemon's own list order inside a host), 335 # of the answer rather than assumed.
323 # and `/ws/<id>` is read out of the answer rather than assumed.
324 for _t in 0 1 2; do 336 for _t in 0 1 2; do
325 wait_for "$OUT.dwh" "tile $_t:" 15 || { 337 wait_for "$OUT.dwh" "tile $_t:" 15 || {
326 echo "e2e FAIL: host wall: tile $_t never appeared; the hub said:" 338 echo "e2e FAIL: host wall: tile $_t never appeared; the hub said:"
@@ -330,11 +342,11 @@ done
330 # separate greps would pass on a wall holding them in any order at all. 342 # separate greps would pass on a wall holding them in any order at all.
331 dw_shape() { tiles_shape "$DWORIG"; } 343 dw_shape() { tiles_shape "$DWORIG"; }
332 dw_id_of() { tile_id_of "$DWORIG" "$1" "$2"; } 344 dw_id_of() { tile_id_of "$DWORIG" "$1" "$2"; }
333 DWWANT="[{\"label\":\"--sock $SOCK25\",\"session\":\"0\"}," 345 DWWANT="[{\"label\":\"--sock $SOCK76\",\"session\":\"0\"},"
334 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"}," 346 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"},"
335 DWWANT="$DWWANT{\"label\":\"--sock $SOCK76\",\"session\":\"0\"}]" 347 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"0\"}]"
336 [ "$(dw_shape)" = "$DWWANT" ] || { 348 [ "$(dw_shape)" = "$DWWANT" ] || {
337 echo "e2e FAIL: host wall: /tiles is not host order then daemon order" 349 echo "e2e FAIL: host wall: /tiles is not the layout file's leaf order"
338 echo " got: $(dw_shape)" 350 echo " got: $(dw_shape)"
339 echo " want: $DWWANT"; exit 1; } 351 echo " want: $DWWANT"; exit 1; }
340 # The ids this leg then names. Read from the answer, so a hub that numbered 352 # The ids this leg then names. Read from the answer, so a hub that numbered
@@ -345,6 +357,32 @@ DWID_B0=$(dw_id_of "--sock $SOCK76" 0)
345 [ -n "$DWID_A0" ] && [ -n "$DWID_AB" ] && [ -n "$DWID_B0" ] || { 357 [ -n "$DWID_A0" ] && [ -n "$DWID_AB" ] && [ -n "$DWID_B0" ] || {
346 echo "e2e FAIL: host wall: could not read the three tile ids out of:" 358 echo "e2e FAIL: host wall: could not read the three tile ids out of:"
347 curl -s "$DWORIG/tiles"; exit 1; } 359 curl -s "$DWORIG/tiles"; exit 1; }
360 # Every pane says how its poller graded it, which is the field the page
361 # reads to decide what to paint. Three live sessions on two answering
362 # daemons, so nothing here is `gone`.
363 curl -s "$DWORIG/tiles" | grep -q '"state":"gone"' && {
364 echo "e2e FAIL: host wall: a pane on a live session was graded gone:"
365 curl -s "$DWORIG/tiles"; exit 1; }
366
367 # A session born ANYWHERE ELSE is not on this wall. It is a live session on
368 # a listed daemon that the hub is polling once a second, and the only
369 # reason it gets no tile is that no leaf names it — which is the whole of
370 # the model. Two and a half seconds is several poll answers.
371 hostroom dwoff
372 pipe_mux "$OUT.dwoff" "$OUT.dwoff.err" env XDG_STATE_HOME="$HOSTROOM" timeout 40 \
373 "$MUX" --sock "$SOCK25" --session offwall
374 pipe_send 'printf "dwoff-%%s\\n" mark\n'
375 await_out "$OUT.dwoff" "dwoff-mark" "host wall: the off-wall session never came up"
376 pipe_detach "host wall: the off-wall session's maker"
377 wait_grid "$SOCK25" "dwoff-mark" "host wall: the off-wall session's marker" offwall
378 sleep 2.5
379 [ "$(dw_shape)" = "$DWWANT" ] || {
380 echo "e2e FAIL: host wall: a session born off the wall became a tile; /tiles holds:"
381 curl -s "$DWORIG/tiles"; exit 1; }
382 # ...and it is not in the file either: nothing but a `+` writes a pane.
383 grep -qF -- "--sock $SOCK25#offwall" "$DWSTATE/mux/layout" && {
384 echo "e2e FAIL: host wall: a session born off the wall was written into the layout:"
385 cat "$DWSTATE/mux/layout"; exit 1; }
348 386
349 # A tile is a REAL session: a marker typed through the CLI door on A#b is 387 # A tile is a REAL session: a marker typed through the CLI door on A#b is
350 # read back through the WebSocket door of the tile that names it — A#b's 388 # read back through the WebSocket door of the tile that names it — A#b's
@@ -402,12 +440,15 @@ RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X DELETE "$DWO
402 curl -s "$DWORIG/tiles"; exit 1; } 440 curl -s "$DWORIG/tiles"; exit 1; }
403 441
404 # `+`: a new session on THAT tile's daemon, named by the daemon — 442 # `+`: a new session on THAT tile's daemon, named by the daemon —
405 # `nextFreeName` off A's own list of `0` and `b`, so `1`. The tile reaches 443 # `nextFreeName` off A's own list, and the pane WRITTEN INTO THE FILE
406 # the wall through the POLL, never through this reply: one road on. 444 # beside the one it was born from. Two roads would be two walls, so there
445 # is one: the birth appends the leaf and the hub seats a tile for it.
407 SESS_BEFORE=$("$MUX" d stats --sock "$SOCK25" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p') 446 SESS_BEFORE=$("$MUX" d stats --sock "$SOCK25" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p')
408 [ -n "$SESS_BEFORE" ] || { 447 [ -n "$SESS_BEFORE" ] || {
409 echo "e2e FAIL: host wall: no sessions= in stats (the field moved?); it says:" 448 echo "e2e FAIL: host wall: no sessions= in stats (the field moved?); it says:"
410 "$MUX" d stats --sock "$SOCK25"; exit 1; } 449 "$MUX" d stats --sock "$SOCK25"; exit 1; }
450 # A's list is `0`, `b` and the `offwall` session made above, so the lowest
451 # free integer beside them is `1`.
411 R=$(curl -s -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_A0") 452 R=$(curl -s -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_A0")
412 [ "$R" = '{"session":"1"}' ] || { 453 [ "$R" = '{"session":"1"}' ] || {
413 echo "e2e FAIL: host wall: spawn on A's tile returned $R, want session 1"; exit 1; } 454 echo "e2e FAIL: host wall: spawn on A's tile returned $R, want session 1"; exit 1; }
@@ -427,10 +468,50 @@ RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X POST "$DWORI
427 DWID_A1=$(dw_id_of "--sock $SOCK25" 1) 468 DWID_A1=$(dw_id_of "--sock $SOCK25" 1)
428 [ -n "$DWID_A1" ] || { echo "e2e FAIL: host wall: the born tile has no id"; exit 1; } 469 [ -n "$DWID_A1" ] || { echo "e2e FAIL: host wall: the born tile has no id"; exit 1; }
429 470
430 # A session that ENDS leaves the wall, and the shell is gone per the OS 471 # A second `+`, on the OTHER daemon's pane. Its session is called `1` too —
431 # rather than per the hub. The pid is read off the GRID — the shell's own 472 # B's own list is just `0` — and it must be a different TILE: ids are the
432 # `$$` — because a daemon reporting on its own children cannot catch itself 473 # hub's, not each host's, and a browser holds `/ws/<id>` across every
433 # being wrong about whether one is still there. 474 # change it did not make, so an id handed out twice would silently
475 # re-point that socket at a different shell on a different machine.
476 R=$(curl -s -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_B0")
477 [ "$R" = '{"session":"1"}' ] || {
478 echo "e2e FAIL: host wall: spawn on B's tile returned $R, want session 1"; exit 1; }
479 wait_until 60 "host wall: B's birth never became a tile; the wall holds:" \
480 '[ -n "$(dw_id_of "--sock $SOCK76" 1)" ]' 'curl -s "$DWORIG/tiles"'
481 DWID_B1=$(dw_id_of "--sock $SOCK76" 1)
482 [ "$DWID_B1" != "$DWID_A1" ] || {
483 echo "e2e FAIL: host wall: two daemons' session 1 came back on ONE id $DWID_A1"
484 curl -s "$DWORIG/tiles"; exit 1; }
485
486 # The FILE is the wall, so both panes have to be in it — each immediately
487 # after the leaf it was born beside, which is where `Tree.insert` puts a
488 # sibling and where a terminal wall opening this file will paint it. B's
489 # lands SECOND, in the middle of the file, which is what says the anchor
490 # and not the end is where a birth goes.
491 DWLEAVES=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$DWSTATE/mux/layout" | tr '\n' ' ')
492 DWLWANT="--sock $SOCK76#0 --sock $SOCK76#1 --sock $SOCK25#b --sock $SOCK25#0 --sock $SOCK25#1 "
493 [ "$DWLEAVES" = "$DWLWANT" ] || {
494 echo "e2e FAIL: host wall: a born pane is not beside the one it was born from"
495 echo " got: $DWLEAVES"
496 echo " want: $DWLWANT"
497 cat "$DWSTATE/mux/layout"; exit 1; }
498 # ...and `/tiles` says the same, in the same order: one wall, read twice.
499 DWWANT="[{\"label\":\"--sock $SOCK76\",\"session\":\"0\"},"
500 DWWANT="$DWWANT{\"label\":\"--sock $SOCK76\",\"session\":\"1\"},"
501 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"},"
502 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"0\"},"
503 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"1\"}]"
504 [ "$(dw_shape)" = "$DWWANT" ] || {
505 echo "e2e FAIL: host wall: /tiles and the layout disagree about the born panes"
506 echo " got: $(dw_shape)"
507 echo " want: $DWWANT"; exit 1; }
508
509 # A session that ENDS dresses its pane `gone` and does not take it: the
510 # pane is a rect the user wrote down and an offer to start it again, not a
511 # session. The shell is gone per the OS rather than per the hub — the pid
512 # is read off the GRID, the shell's own `$$`, because a daemon reporting on
513 # its own children cannot catch itself being wrong about whether one is
514 # still there.
434 timeout 20 "$MUX" a send 'printf "bornpid-%s\n" $$\n' --sock "$SOCK25" --session 1 > /dev/null 2>&1 515 timeout 20 "$MUX" a send 'printf "bornpid-%s\n" $$\n' --sock "$SOCK25" --session 1 > /dev/null 2>&1
435 wait_until 80 "host wall: the born session never said its own pid; it holds:" \ 516 wait_until 80 "host wall: the born session never said its own pid; it holds:" \
436 'timeout 20 "$MUX" a capture --sock "$SOCK25" --session 1 > "$OUT.dwpid" 2>&1; grep -q "bornpid-[0-9]" "$OUT.dwpid"' \ 517 'timeout 20 "$MUX" a capture --sock "$SOCK25" --session 1 > "$OUT.dwpid" 2>&1; grep -q "bornpid-[0-9]" "$OUT.dwpid"' \
@@ -440,40 +521,35 @@ DWSHELL=$(sed -n 's/.*bornpid-\([0-9][0-9]*\).*/\1/p' "$OUT.dwpid" | head -1)
440 echo "e2e FAIL: host wall: the born session never said its own pid; it holds:" 521 echo "e2e FAIL: host wall: the born session never said its own pid; it holds:"
441 cat "$OUT.dwpid"; exit 1; } 522 cat "$OUT.dwpid"; exit 1; }
442 timeout 20 "$MUX" a send 'exit\n' --sock "$SOCK25" --session 1 > /dev/null 2>&1 || true 523 timeout 20 "$MUX" a send 'exit\n' --sock "$SOCK25" --session 1 > /dev/null 2>&1 || true
443 wait_until 80 "host wall: the ended session's tile never left; the wall holds:" \ 524 wait_until 80 "host wall: the ended session's pane never wore gone; the wall holds:" \
444 '! curl -s "$DWORIG/tiles" | grep -q "\"session\":\"1\""' 'curl -s "$DWORIG/tiles"' 525 'curl -s "$DWORIG/tiles" | tr "{" "\n" | grep -q "\"session\":\"1\",\"state\":\"gone\""' \
526 'curl -s "$DWORIG/tiles"'
445 # Asked of the OS, not of the daemon. 527 # Asked of the OS, not of the daemon.
446 [ ! -e "/proc/$DWSHELL" ] || { 528 [ ! -e "/proc/$DWSHELL" ] || {
447 echo "e2e FAIL: host wall: the shell pid $DWSHELL outlived its session per /proc" 529 echo "e2e FAIL: host wall: the shell pid $DWSHELL outlived its session per /proc"
448 cat "/proc/$DWSHELL/cmdline" 2>/dev/null; exit 1; } 530 cat "/proc/$DWSHELL/cmdline" 2>/dev/null; exit 1; }
449 531 # The pane STAYS, on its own id and in its own place: five leaves, and the
450 # The same name again, and it must NOT be the same tile. The browser holds 532 # ended one still among them. A hub that vanished a graded pane would take
451 # `/ws/<id>` across a vanishing it did not make, so an id handed out twice 533 # a rect the user authored on the strength of one poll answer.
452 # would silently re-point that socket at a different shell — a different 534 [ "$(dw_id_of "--sock $SOCK25" 1)" = "$DWID_A1" ] || {
453 # shell that happens to have the name the first one had is exactly the case 535 echo "e2e FAIL: host wall: the ended session's pane left or changed id;"
454 # a per-name id would get wrong. 536 echo " the wall holds:"; curl -s "$DWORIG/tiles"; exit 1; }
455 R=$(curl -s -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_A0") 537
456 [ "$R" = '{"session":"1"}' ] || { 538 # An id the wall does not have is answered in HTTP: a 404 the page can
457 echo "e2e FAIL: host wall: the second spawn returned $R, want session 1 again"; exit 1; } 539 # read, rather than an upgrade followed by a silent close it can only
458 wait_until 60 "host wall: the second birth never became a tile; the wall holds:" \ 540 # guess at. A page holding one is not hypothetical — a hub restarted on a
459 '[ -n "$(dw_id_of "--sock $SOCK25" 1)" ]' 'curl -s "$DWORIG/tiles"' 541 # shorter file hands out fewer ids than the last one did. The stand-in
460 [ "$(dw_id_of "--sock $SOCK25" 1)" != "$DWID_A1" ] || { 542 # sees a non-101 and exits 4, the wrong-Origin shape.
461 echo "e2e FAIL: host wall: session 1 came back on the SAME id $DWID_A1"
462 curl -s "$DWORIG/tiles"; exit 1; }
463
464 # ...and the id that left is answered in HTTP: a 404 the page can read,
465 # rather than an upgrade followed by a silent close it can only guess at.
466 # The stand-in sees a non-101 and exits 4, the wrong-Origin shape.
467 set +e 543 set +e
468 timeout 20 "$WSCLIENT" --port "$WPORT4" --tile "$DWID_A1" \ 544 timeout 20 "$WSCLIENT" --port "$WPORT4" --tile 999 \
469 --out "$OUT.dwdead" --err "$OUT.dwdead.err" < /dev/null 545 --out "$OUT.dwdead" --err "$OUT.dwdead.err" < /dev/null
470 RC=$? 546 RC=$?
471 set -e 547 set -e
472 [ "$RC" -eq 4 ] || { 548 [ "$RC" -eq 4 ] || {
473 echo "e2e FAIL: host wall: wsclient on a departed tile exited $RC, want 4" 549 echo "e2e FAIL: host wall: wsclient on an unknown tile exited $RC, want 4"
474 cat -v "$OUT.dwdead.err" 2>/dev/null; exit 1; } 550 cat -v "$OUT.dwdead.err" 2>/dev/null; exit 1; }
475 grep -q "upgrade refused.*404" "$OUT.dwdead.err" || { 551 grep -q "upgrade refused.*404" "$OUT.dwdead.err" || {
476 echo "e2e FAIL: host wall: a dead id was not refused with a 404:" 552 echo "e2e FAIL: host wall: an unknown id was not refused with a 404:"
477 cat -v "$OUT.dwdead.err"; exit 1; } 553 cat -v "$OUT.dwdead.err"; exit 1; }
478 554
479 # A daemon that goes AWAY is not a session that ended: an unreachable poll 555 # A daemon that goes AWAY is not a session that ended: an unreachable poll
@@ -538,4 +614,4 @@ assert_stopped "$SOCK76" "$D23PID" "host wall: daemon B" "$OUT.dwstopb2"
538 D23PID="" 614 D23PID=""
539 assert_stopped "$SOCK25" "$D22PID" "host wall: daemon A" "$OUT.dwstop" 615 assert_stopped "$SOCK25" "$D22PID" "host wall: daemon A" "$OUT.dwstop"
540 D22PID="" 616 D22PID=""
541 ok "the hub's wall is the hosts file; tiles are those daemons' live sessions" 617 ok "the hub's wall is the layout: /tiles lists its leaves in order, a session born elsewhere is not listed, and + writes the new pane into the file"
test/e2e_07_wallcli.sh
Old New
@@ -54,10 +54,13 @@ defer_sock "$SOCK31"
54 # 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
55 # asserted order-free by the greps on the capture below. 55 # asserted order-free by the greps on the capture below.
56 # 56 #
57 # Nothing on the command line names a tile any more: the wall is the 57 # Nothing on the command line names the second tile: `a` is named because
58 # DAEMON, and the tiles are whatever sessions it turns out to have. `a` is 58 # the wall is entered zoomed on it, and `b` comes from the LAYOUT — the
59 # named only because the wall is entered zoomed on it; `b` and the 59 # file is the wall now, so a pane is a leaf somebody wrote down. `z` is
60 # daemon's own `0` arrive from the host's list, which is the claim. 60 # the control: a live session on the same daemon that no leaf names, and
61 # it must be nowhere on this terminal. Without it the leg could not tell
62 # "the layout put b here" from "every session the daemon has lands here",
63 # which is exactly the model that was replaced.
61 start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh 64 start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh
62 D23PID=$DPID 65 D23PID=$DPID
63 66
@@ -71,13 +74,18 @@ pipe_send 'printf "cwb-%%s\\n" pin\n'
71 await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client" 74 await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client"
72 pipe_detach 75 pipe_detach
73 wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b 76 wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b
77 pipe_mux "$OUT.cwz" "$OUT.cwz.err" env XDG_STATE_HOME="$CWSTATE" timeout 40 "$MUX" --sock "$SOCK26" --session z
78 pipe_send 'printf "cwz-%%s\\n" pin\n'
79 await_out "$OUT.cwz" "cwz-pin" "cwz-pin never reached the client"
80 pipe_detach
81 wait_grid "$SOCK26" "cwz-pin" "CLI wall: session z's marker" z
74 82
75 ( sleep 5; "$MUX" a send 'printf "cwlive-%s\n" pin\n' \ 83 ( sleep 5; "$MUX" a send 'printf "cwlive-%s\n" pin\n' \
76 --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) & 84 --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) &
77 CWINJPID=$! 85 CWINJPID=$!
78 defer_kill "$CWINJPID" 86 defer_kill "$CWINJPID"
79 set +e 87 set +e
80 no_saved_tree "$CWSTATE" 88 seed_layout "$CWSTATE" stacked "--sock $SOCK26#a" "--sock $SOCK26#b"
81 XDG_STATE_HOME="$CWSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 \ 89 XDG_STATE_HOME="$CWSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 \
82 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \ 90 --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \
83 "$MUX" --sock "$SOCK26" --session a > "$OUT.cwpc" 2>&1 <<'EOF' 91 "$MUX" --sock "$SOCK26" --session a > "$OUT.cwpc" 2>&1 <<'EOF'
@@ -98,12 +106,20 @@ grep -q "cwb-pin" "$OUT.cwcap" || {
98 echo "e2e FAIL: CLI wall: b's marker never painted"; exit 1; } 106 echo "e2e FAIL: CLI wall: b's marker never painted"; exit 1; }
99 grep -q "cwlive-pin" "$OUT.cwcap" || { 107 grep -q "cwlive-pin" "$OUT.cwcap" || {
100 echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; } 108 echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; }
109 # ...and the session no leaf names is nowhere on this terminal. It was
110 # live on the same daemon throughout — `wait_grid` above says the daemon
111 # really holds it — so its absence here is the layout deciding what the
112 # wall shows and not the daemon having nothing to offer.
113 grep -q "cwz-pin" "$OUT.cwcap" && {
114 echo "e2e FAIL: CLI wall: session z is on the wall and no leaf names it —"
115 echo " the wall is taking its panes from the daemon's list again:"
116 cat "$OUT.cwpc"; exit 1; }
101 # Every tile claims its rect: a and b were created 80-wide by pipe, and 117 # Every tile claims its rect: a and b were created 80-wide by pipe, and
102 # this 40-wide wall resized both on attach. There is no 0x0 passivity to 118 # this 40-wide wall resized both on attach. There is no 0x0 passivity to
103 # assert — the wall writes the sessions it shows. 119 # assert — the wall writes the sessions it shows.
104 assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop" 120 assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop"
105 D23PID="" 121 D23PID=""
106 ok "mux --sock: every session the daemon has, and a live delta, on one terminal" 122 ok "mux --sock: the entry pane and the layout's other panes, and a live delta, on one terminal"
107 123
108 # ---- M1: Ctrl-\ is a prefix, not an action ------------------------------ 124 # ---- M1: Ctrl-\ is a prefix, not an action ------------------------------
109 # 125 #
@@ -230,15 +246,26 @@ grep -q "m2b-pin" "$OUT.nswcap1" || {
230 echo "e2e FAIL: new session: the chord's own session never got the marker:" 246 echo "e2e FAIL: new session: the chord's own session never got the marker:"
231 cat "$OUT.nswcap1"; exit 1; } 247 cat "$OUT.nswcap1"; exit 1; }
232 248
233 # Six more, so the wall the next client opens is eight tiles on 24 rows — 249 # Six more, and then a layout naming all eight, so the wall the next
234 # saturated, with nothing left over. Exactly saturated on purpose: a wall 250 # client opens is eight tiles on 24 rows — saturated, with nothing left
235 # with sessions it cannot place says `[+N not shown]` on every poll, and 251 # over. The sessions are made first and the file written second because
236 # that notice would overwrite the one this leg is waiting to read. 252 # the seed refuses a leaf whose host is not listed, not one whose session
253 # is not live; making them anyway keeps the fixture a wall of eight real
254 # panes rather than eight `[session ended]` rects, which have no pump to
255 # refuse a ninth on top of.
256 #
257 # Authored, not polled: the file is the wall, so eight tiles is eight
258 # leaves. Exactly saturated on purpose — a wall with a pane it cannot cut
259 # says so on the notice line, and that notice would overwrite the one this
260 # leg is waiting to read.
237 fill_sessions "$SOCK28" "$OUT.nswfill" fill 2 7 261 fill_sessions "$SOCK28" "$OUT.nswfill" fill 2 7
238 wait_sessions "$SOCK28" 8 "new session: the wall's terminal should be exactly full" 262 wait_sessions "$SOCK28" 8 "new session: the wall's terminal should be exactly full"
239 263
240 set +e 264 set +e
241 no_saved_tree "$NSWSTATE" 265 seed_layout "$NSWSTATE" stacked \
266 "--sock $SOCK28#0" "--sock $SOCK28#1" \
267 "--sock $SOCK28#fill2" "--sock $SOCK28#fill3" "--sock $SOCK28#fill4" \
268 "--sock $SOCK28#fill5" "--sock $SOCK28#fill6" "--sock $SOCK28#fill7"
242 XDG_STATE_HOME="$NSWSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 24 \ 269 XDG_STATE_HOME="$NSWSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 24 \
243 --out "$OUT.nsw2" --err "$OUT.nsw2.err" \ 270 --out "$OUT.nsw2" --err "$OUT.nsw2.err" \
244 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw2.log" 2>&1 <<'EOF' 271 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw2.log" 2>&1 <<'EOF'
test/e2e_08_mouse.sh
Old New
@@ -356,8 +356,8 @@ wait_grid "$SOCK37" "zsb-pin" "focus skip: session b's marker" b
356 ZSATT_BEFORE=$(attaches_now "$SOCK37") 356 ZSATT_BEFORE=$(attaches_now "$SOCK37")
357 watch_clients "$SOCK37" "$OUT.zswatch" 357 watch_clients "$SOCK37" "$OUT.zswatch"
358 set +e 358 set +e
359 # tall: the aspect rule would cut columns at 92x30 (92 >= 60) 359 # tall: two stacked stripes, so a focus step is a step down the screen
360 no_saved_tree "$ZSSTATE" 360 seed_layout "$ZSSTATE" stacked "--sock $SOCK37#0" "--sock $SOCK37#b"
361 XDG_STATE_HOME="$ZSSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ 361 XDG_STATE_HOME="$ZSSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \
362 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \ 362 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \
363 "$MUX" --sock "$SOCK37" > "$OUT.zspc" 2>&1 <<'EOF' 363 "$MUX" --sock "$SOCK37" > "$OUT.zspc" 2>&1 <<'EOF'
@@ -594,14 +594,20 @@ printf -- '--sock %s\n--sock %s\n' "$SOCKMC1" "$SOCKMC2" > "$MCSTATE/mux/hosts"
594 start_daemon "$SOCKMC1" "$OUT.mc1.d" "dead-click daemon 1 never bound" --shell /bin/sh 594 start_daemon "$SOCKMC1" "$OUT.mc1.d" "dead-click daemon 1 never bound" --shell /bin/sh
595 start_daemon "$SOCKMC2" "$OUT.mc2.d" "dead-click daemon 2 never bound" --shell /bin/sh 595 start_daemon "$SOCKMC2" "$OUT.mc2.d" "dead-click daemon 2 never bound" --shell /bin/sh
596 MC2PID=$DPID 596 MC2PID=$DPID
597 # A polled host never CREATES a session, and a fresh daemon has none: host 597 # A polled host never CREATES a session: host 2's pane binds only if the
598 # 2's tile exists only if something attached there first. One attach-and- 598 # session its leaf names is really there. One attach-and-detach births the
599 # detach births its default session; the daemon keeps it. 599 # default session; the daemon keeps it.
600 pipe_mux "$OUT.mc0" "$OUT.mc0.err" env XDG_STATE_HOME="$MCSTATE" timeout 40 "$MUX" --sock "$SOCKMC2" 600 pipe_mux "$OUT.mc0" "$OUT.mc0.err" env XDG_STATE_HOME="$MCSTATE" timeout 40 "$MUX" --sock "$SOCKMC2"
601 pipe_detach 601 pipe_detach
602 # ...and the wall that shows it, written down: a poll adds no pane, so the
603 # two-pane cut this leg clicks across is authored here. `beside` because
604 # the clicks below are at columns 75 and 25 — left pane and right pane, on
605 # a 100-column terminal.
606 seed_layout "$MCSTATE" beside "--sock $SOCKMC1#0" "--sock $SOCKMC2#0"
602 607
603 # Run 1: both hosts up, a wall of two, left on a terminal — the layout 608 # Run 1: both hosts up, a wall of two, left on a terminal — the layout is
604 # sidecar is what carries host 2's pane into run 2 as a saved pane. 609 # what carries host 2's pane into run 2 as a saved pane whose daemon has
610 # gone.
605 set +e 611 set +e
606 XDG_STATE_HOME="$MCSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ 612 XDG_STATE_HOME="$MCSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
607 --out "$OUT.mc1" --err "$OUT.mc1.err" -- \ 613 --out "$OUT.mc1" --err "$OUT.mc1.err" -- \
test/e2e_09_hosts.sh
Old New
@@ -3,11 +3,12 @@
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 # 5 #
6 # The wall is a list of DAEMONS: two of them here, because the claim is 6 # The wall is a LAYOUT over a list of daemons: two daemons here, because
7 # that the tiles come from the daemons and not from a file, and one daemon 7 # a pane names a host and a session and one daemon cannot show which of the
8 # cannot show which of the two a tile belongs to. Both daemons have a 8 # two a pane belongs to. Both daemons have a session named `0` for exactly
9 # session named `0` for exactly that reason — a wall that keyed tiles on 9 # that reason — a wall that keyed panes on the session name alone would
10 # the session name alone would show one of them twice. 10 # show one of them twice. Every leg that wants panes writes them down
11 # (`seed_layout`): a poll adds none.
11 # 12 #
12 # A state home of its own, and one per leg that reads the file back: every 13 # A state home of its own, and one per leg that reads the file back: every
13 # `mux` in this suite now records the DAEMON it attached to, so a hosts 14 # `mux` in this suite now records the DAEMON it attached to, so a hosts
@@ -49,13 +50,14 @@ DH1PID=$DPID
49 start_daemon "$SOCKH2" "$OUT.h2.d" "hosts daemon 2 never bound" --shell /bin/sh 50 start_daemon "$SOCKH2" "$OUT.h2.d" "hosts daemon 2 never bound" --shell /bin/sh
50 DH2PID=$DPID 51 DH2PID=$DPID
51 52
52 # hosts_where NEEDLE — which of the four live sessions on the two daemons 53 # hosts_where NEEDLE — which of the five sessions these two daemons hold
53 # hold NEEDLE, one `SOCK:NAME` per line. The wall's tiles are read back 54 # over the group's life holds NEEDLE, one `SOCK:NAME` per line. A pane's
54 # from the DAEMONS that own them, never from the terminal: a marker on 55 # content is read back from the DAEMON that owns it, never from the
55 # screen says a stripe painted, and what these legs claim is which SESSION 56 # terminal: a marker on screen says a stripe painted, and what these legs
56 # a keystroke reached. 57 # claim is which SESSION a keystroke reached. Sessions that do not exist
58 # at the moment of the call simply do not answer.
57 hosts_where() { 59 hosts_where() {
58 for _hw in "$SOCKH1:0" "$SOCKH1:b" "$SOCKH2:0" "$SOCKH2:c"; do 60 for _hw in "$SOCKH1:0" "$SOCKH1:b" "$SOCKH1:d" "$SOCKH2:0" "$SOCKH2:c"; do
59 if dump_session "${_hw%%:*}" "${_hw##*:}" 2>/dev/null | grep -q "$1"; then 61 if dump_session "${_hw%%:*}" "${_hw##*:}" 2>/dev/null | grep -q "$1"; then
60 printf '%s\n' "$_hw" 62 printf '%s\n' "$_hw"
61 fi 63 fi
@@ -93,9 +95,13 @@ fi
93 95
94 # The listed-local half. The file is written by hand rather than by an 96 # The listed-local half. The file is written by hand rather than by an
95 # attach, because what is under test is a line whose daemon is GONE — the 97 # attach, because what is under test is a line whose daemon is GONE — the
96 # state every hosts file is in after a reboot. 98 # state every hosts file is in after a reboot. A layout beside it naming
99 # that daemon's own session, because the wall is the layout: without a
100 # leaf the wall comes up empty and the keystrokes below would go to the
101 # picker the emptiness opens instead of to a shell.
97 mkdir -p "$HASTATE/mux" "$HARUN" 102 mkdir -p "$HASTATE/mux" "$HARUN"
98 printf -- '--sock %s\n' "$HASOCK" > "$HASTATE/mux/hosts" 103 printf -- '--sock %s\n' "$HASOCK" > "$HASTATE/mux/hosts"
104 seed_layout "$HASTATE" stacked "--sock $HASOCK#0"
99 [ ! -S "$HASOCK" ] || { echo "e2e FAIL: hosts: the auto-start path is already served"; exit 1; } 105 [ ! -S "$HASOCK" ] || { echo "e2e FAIL: hosts: the auto-start path is already served"; exit 1; }
100 set +e 106 set +e
101 # PATH so the ssh recipes resolve exactly the binary under test, and 107 # PATH so the ssh recipes resolve exactly the binary under test, and
@@ -132,6 +138,13 @@ HASH_PID=$("$MUX" d dump --sock "$HASOCK" | sed -n 's/.*hapid=\([0-9]*\).*/\1/p'
132 [ "$(ps -o ppid= -p "$HASH_PID" 2>/dev/null | tr -d ' ')" = "$(real_pid "$HAPID")" ] || { 138 [ "$(ps -o ppid= -p "$HASH_PID" 2>/dev/null | tr -d ' ')" = "$(real_pid "$HAPID")" ] || {
133 echo "e2e FAIL: hosts: the shell on the grid is not the auto-started daemon's child" 139 echo "e2e FAIL: hosts: the shell on the grid is not the auto-started daemon's child"
134 ps -o pid,ppid,args -p "$HASH_PID" 2>&1; exit 1; } 140 ps -o pid,ppid,args -p "$HASH_PID" 2>&1; exit 1; }
141 # ...and the wall wrote its panes back: one leaf, the session that pane
142 # held, named exactly once. The wall the user gets back is the wall they
143 # left, and a save that appended a second copy of a leaf would be a file
144 # `seedLayout` refuses whole on the next start.
145 [ "$(grep -c -- "--sock $HASOCK#0\$" "$HASTATE/mux/layout")" = "1" ] || {
146 echo "e2e FAIL: hosts: the auto-start wall did not save its one pane exactly once:"
147 cat "$HASTATE/mux/layout"; exit 1; }
135 assert_stopped "$HASOCK" "$HAPID" "hosts auto-start" "$OUT.hastop" 148 assert_stopped "$HASOCK" "$HAPID" "hosts auto-start" "$OUT.hastop"
136 HAPID="" 149 HAPID=""
137 ok "mux records the daemon on the wall, never a session, and starts a listed local one that is gone" 150 ok "mux records the daemon on the wall, never a session, and starts a listed local one that is gone"
@@ -186,26 +199,42 @@ HESH_PID=$("$MUX" d dump --sock "$HESOCK" | sed -n 's/.*hepid=\([0-9]*\).*/\1/p'
186 [ "$(ps -o ppid= -p "$HESH_PID" 2>/dev/null | tr -d ' ')" = "$(real_pid "$HEPID")" ] || { 199 [ "$(ps -o ppid= -p "$HESH_PID" 2>/dev/null | tr -d ' ')" = "$(real_pid "$HEPID")" ] || {
187 echo "e2e FAIL: hosts: the shell on the grid is not the auto-started daemon's child" 200 echo "e2e FAIL: hosts: the shell on the grid is not the auto-started daemon's child"
188 ps -o pid,ppid,args -p "$HESH_PID" 2>&1; exit 1; } 201 ps -o pid,ppid,args -p "$HESH_PID" 2>&1; exit 1; }
202 # ...and the LAYOUT the detach wrote: one leaf, the session the entry
203 # attach opened on. Two files, both written by a `mux` that was handed
204 # neither — the hosts file says which daemons, the layout says which panes,
205 # and a machine with no files at all ends up with a wall of one either way.
206 _he_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$HESTATE/mux/layout" | tr '\n' ' ')
207 [ "$_he_leaves" = "--sock $HESOCK#0 " ] || {
208 echo "e2e FAIL: hosts: the first mux saved leaves '$_he_leaves',"
209 echo " want the one pane it attached to; the file holds:"
210 cat "$HESTATE/mux/layout" 2>&1; exit 1; }
189 assert_stopped "$HESOCK" "$HEPID" "hosts empty file" "$OUT.hestop" 211 assert_stopped "$HESOCK" "$HEPID" "hosts empty file" "$OUT.hestop"
190 HEPID="" 212 HEPID=""
191 ok "the first mux on a machine starts the local daemon and writes it down" 213 ok "the first mux on a machine starts the local daemon and writes it down"
192 214
193 # ---- every live session of every listed daemon is a tile ---------------- 215 # ---- a wall shows its layout's panes and no more -------------------------
194 # 216 #
195 # The headline. Two daemons on the wall, four sessions between them, and 217 # The headline, and the model in one leg. Two daemons on the wall, four
196 # the wall is asked for all four AT ONCE — on a real pty, because a stripe 218 # sessions between them, and a LAYOUT naming three of them — two on daemon
197 # is a rect on a terminal and a pipe has none. 219 # 1 and one on daemon 2. On a real pty, because a stripe is a rect on a
220 # terminal and a pipe has none.
198 # 221 #
199 # Three claims: 222 # Four claims:
200 # 223 #
201 # * four bars, and each names the daemon it belongs to. Both daemons have 224 # * the three leaves are three bars, each naming the daemon it belongs
202 # a session called `0`; a wall keyed on the session name would paint 225 # to. Both daemons have a session called `0`; a wall keyed on the
203 # three tiles and call it four. 226 # session name would paint two tiles and call it three.
204 # * `Ctrl-\ n` walks them. Where the two markers LANDED is read off the 227 # * the fourth session — daemon 2's `c`, live on a listed, answering,
205 # daemons, so a step that only moved a `>` on screen fails here. 228 # once-a-second-polled daemon — is NOWHERE. That is the whole change:
206 # * `Ctrl-\ c` births a session, and the file does not move. The wall is 229 # until 2026-09-02 a host's list was where tiles came from, so every
207 # the daemons' truth: a second `mux` finds the new session as a tile 230 # session on every listed box was on every wall over it.
208 # with nothing written down anywhere. 231 # * a session born while the wall is up is nowhere either, and a wall
232 # opened AFTER it still does not have it. The poll grades panes; it
233 # does not add them.
234 # * `Ctrl-\ n` walks exactly the three and wraps to the first. Where each
235 # marker LANDED is read off the daemons, so a step that only moved a
236 # `>` on screen fails here, and a fourth session quietly on the ring
237 # would show as the wrap landing somewhere new.
209 env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts add "--sock $SOCKH2" > "$OUT.hadd" 2>&1 || { 238 env XDG_STATE_HOME="$HSTATE" timeout 40 "$MUX" hosts add "--sock $SOCKH2" > "$OUT.hadd" 2>&1 || {
210 echo "e2e FAIL: hosts: 'hosts add' refused the second daemon:" 239 echo "e2e FAIL: hosts: 'hosts add' refused the second daemon:"
211 cat "$OUT.hadd"; exit 1; } 240 cat "$OUT.hadd"; exit 1; }
@@ -214,8 +243,9 @@ pipe_send 'printf "hb-%%s\\n" pin; echo bshell=$$\n'
214 await_out "$OUT.hb" "hb-pin" "hb-pin never reached the client" 243 await_out "$OUT.hb" "hb-pin" "hb-pin never reached the client"
215 pipe_detach "hosts session b" 244 pipe_detach "hosts session b"
216 wait_grid "$SOCKH1" "hb-pin" "hosts: session b's marker" b 245 wait_grid "$SOCKH1" "hb-pin" "hosts: session b's marker" b
217 # The pid the shell printed of ITSELF, kept for scenario 5: what "hosts rm 246 # The pid the shell printed of ITSELF, kept for the `hosts rm` leg: what
218 # ends nothing" claims is about a PROCESS, and only the OS can answer that. 247 # "hosts rm ends nothing" claims is about a PROCESS, and only the OS can
248 # answer that.
219 wait_grid "$SOCKH1" "bshell=" "hosts: session b's shell pid" b 249 wait_grid "$SOCKH1" "bshell=" "hosts: session b's shell pid" b
220 BSHELL=$(dump_session "$SOCKH1" b | sed -n 's/.*bshell=\([0-9]*\).*/\1/p' | tail -1) 250 BSHELL=$(dump_session "$SOCKH1" b | sed -n 's/.*bshell=\([0-9]*\).*/\1/p' | tail -1)
221 [ -n "$BSHELL" ] || { echo "e2e FAIL: hosts: session b printed no shell pid"; exit 1; } 251 [ -n "$BSHELL" ] || { echo "e2e FAIL: hosts: session b printed no shell pid"; exit 1; }
@@ -229,116 +259,175 @@ wait_grid "$SOCKH2" "hc-pin" "hosts: session c's marker" c
229 wait_sessions "$SOCKH1" 2 "hosts: daemon 1 should hold 0 and b" 259 wait_sessions "$SOCKH1" 2 "hosts: daemon 1 should hold 0 and b"
230 wait_sessions "$SOCKH2" 2 "hosts: daemon 2 should hold 0 and c" 260 wait_sessions "$SOCKH2" 2 "hosts: daemon 2 should hold 0 and c"
231 261
232 # The file, byte for byte, across a `Ctrl-\ c` that creates a session: the 262 # The wall: three of the four. Daemon 2's `c` is deliberately left out, so
233 # wall's tiles come from the daemons, so a birth writes nothing down. 263 # every later assertion about it is about a session the wall could reach
264 # and does not.
265 #
266 # Tall, and NOT wide: 80 columns is under the aspect rule's
267 # `cols >= 2 * rows`, so a bar gets the full width. Beside, three stripes
268 # would each be twenty-odd columns and `labelText` would spend them on the
269 # state word — the socket path these assertions grep for is the first thing
270 # a narrow bar drops.
271 seed_layout "$HSTATE" stacked \
272 "--sock $SOCKH1#0" "--sock $SOCKH1#b" "--sock $SOCKH2#0"
234 HFHASH=$(sha256sum "$HSTATE/mux/hosts" | cut -d' ' -f1) 273 HFHASH=$(sha256sum "$HSTATE/mux/hosts" | cut -d' ' -f1)
235 set +e 274 set +e
236 # Tall, and NOT wide: five tiles (four sessions plus the one `Ctrl-\ c`
237 # makes) have to fit under the row floor a stripe's label bar leaves, and
238 # 80 columns is under the aspect rule's `cols >= 2 * rows`, so the cut is
239 # stacked and every bar gets the full width. Beside, four stripes would
240 # each be twenty columns and `labelText` would spend them on the state
241 # word — the socket path these assertions grep for is the first thing a
242 # narrow bar drops.
243 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \ 275 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \
244 --out "$OUT.hwcap" --err "$OUT.hwcap.err" -- "$MUX" > "$OUT.hwpc" 2>&1 <<'EOF' 276 --out "$OUT.hwcap" --err "$OUT.hwcap.err" -- "$MUX" > "$OUT.hwpc" 2>&1 <<'EOF'
245 expect hc-pin 25000 277 expect hw1-pin 25000
246 settle 1500 25000 278 settle 1500 25000
247 send printf 'walk-%s\\n' one\n 279 send printf 'walk-%s\\n' one\n
248 expect walk-one 20000 280 expect walk-one 20000
249 settle 800 20000 281 settle 800 20000
250 send \x1cn 282 send \x1cn
251 # The `>` landing on the second tile's bar, not a guess at how long that
252 # takes: `paintLabel` redraws both bars on a focus move, and the unfocused
253 # marker is the same width, so `2> ` appears exactly when the focus has.
254 expect 2> --sock 20000 283 expect 2> --sock 20000
284 settle 800 20000
255 send printf 'walk-%s\\n' two\n 285 send printf 'walk-%s\\n' two\n
256 expect walk-two 20000 286 expect walk-two 20000
257 settle 800 20000 287 settle 800 20000
258 send \x1cc 288 send \x1cn
259 settle 2500 25000 289 expect 3> --sock 20000
290 settle 800 20000
291 send printf 'walk-%s\\n' three\n
292 expect walk-three 20000
293 settle 800 20000
294 send \x1cn
295 expect 1> --sock 20000
296 settle 800 20000
297 send printf 'walk-%s\\n' four\n
298 expect walk-four 20000
299 settle 1500 25000
260 send \x1cd 300 send \x1cd
261 waitexit 15000 301 waitexit 15000
262 EOF 302 EOF
263 RC=$? 303 RC=$?
264 set -e 304 set -e
265 rc0 "hosts: the wall leg exited $RC (did every daemon's sessions become tiles?):" "$OUT.hwpc" 305 rc0 "hosts: the wall leg exited $RC (did the layout's leaves become panes?):" "$OUT.hwpc"
266 # Four bars, each naming its own daemon. 306 # Three bars, each naming its own daemon.
267 for _hl in "$SOCKH1#0" "$SOCKH1#b" "$SOCKH2#0" "$SOCKH2#c"; do 307 for _hl in "$SOCKH1#0" "$SOCKH1#b" "$SOCKH2#0"; do
268 grep -q -- "--sock $_hl \[up\]" "$OUT.hwcap" || { 308 grep -q -- "--sock $_hl \[up\]" "$OUT.hwcap" || {
269 echo "e2e FAIL: hosts: '--sock $_hl' never became a tile on the wall:" 309 echo "e2e FAIL: hosts: the leaf '--sock $_hl' never became a pane on the wall:"
270 cat "$OUT.hwpc"; exit 1; } 310 cat "$OUT.hwpc"; exit 1; }
271 done 311 done
272 # The walk, read off the daemons. One holder each, and not the same one: 312 # ...and the fourth session is nowhere: not a bar, not its own output. Its
273 # `n` moved the focus to a different SESSION, on whichever daemon owns it. 313 # daemon answered every poll for the whole run — daemon 2's OTHER session
314 # is one of the three bars above, so "the host never reported" is not a
315 # way this can pass. `wc -l` on the matches and never `grep -c`, which
316 # counts LINES: a pty capture is escape bytes with almost no newlines.
317 [ "$(grep -aoF -- "$SOCKH2#c" "$OUT.hwcap" | wc -l)" -eq 0 ] || {
318 echo "e2e FAIL: hosts: session c is on a wall whose layout never named it —"
319 echo " the poll is taking panes from the daemon's list again:"
320 cat "$OUT.hwpc"; exit 1; }
321 [ "$(grep -aoF "hc-pin" "$OUT.hwcap" | wc -l)" -eq 0 ] || {
322 echo "e2e FAIL: hosts: session c's own output painted on a wall with no pane for it:"
323 cat "$OUT.hwpc"; exit 1; }
324 # The walk, read off the daemons. One holder each, three different
325 # sessions, and the fourth press back on the first: the ring is the
326 # LAYOUT's panes and has exactly three stops.
274 HW1=$(hosts_where walk-one) 327 HW1=$(hosts_where walk-one)
275 HW2=$(hosts_where walk-two) 328 HW2=$(hosts_where walk-two)
276 [ "$(printf '%s\n' "$HW1" | grep -c .)" = "1" ] || { 329 HW3=$(hosts_where walk-three)
277 echo "e2e FAIL: hosts: 'walk-one' landed in [$HW1], want exactly one session"; exit 1; } 330 HW4=$(hosts_where walk-four)
278 [ "$(printf '%s\n' "$HW2" | grep -c .)" = "1" ] || { 331 for _hp in "one:$HW1" "two:$HW2" "three:$HW3" "four:$HW4"; do
279 echo "e2e FAIL: hosts: 'walk-two' landed in [$HW2], want exactly one session"; exit 1; } 332 [ "$(printf '%s\n' "${_hp#*:}" | grep -c .)" = "1" ] || {
280 [ "$HW1" != "$HW2" ] || { 333 echo "e2e FAIL: hosts: 'walk-${_hp%%:*}' landed in [${_hp#*:}], want exactly one session"
281 echo "e2e FAIL: hosts: Ctrl-\\ n did not move the focus — both markers landed in $HW1"; exit 1; } 334 exit 1; }
282 # The birth wrote nothing down, and the daemon really has one more session. 335 done
336 [ "$HW1" != "$HW2" ] && [ "$HW2" != "$HW3" ] && [ "$HW1" != "$HW3" ] || {
337 echo "e2e FAIL: hosts: Ctrl-\\ n did not walk three distinct panes: [$HW1] [$HW2] [$HW3]"
338 exit 1; }
339 [ "$HW4" = "$HW1" ] || {
340 echo "e2e FAIL: hosts: the fourth Ctrl-\\ n landed in [$HW4], not back on [$HW1] —"
341 echo " the ring has a stop the layout never named"; exit 1; }
342 for _hw in "$HW1" "$HW2" "$HW3"; do
343 [ "$_hw" != "$SOCKH2:c" ] || {
344 echo "e2e FAIL: hosts: a Ctrl-\\ n stop is session c, which no leaf names"; exit 1; }
345 done
346 # The hosts file did not move: a wall is opened FROM these two files and
347 # writes back only to the layout.
283 [ "$HFHASH" = "$(sha256sum "$HSTATE/mux/hosts" | cut -d' ' -f1)" ] || { 348 [ "$HFHASH" = "$(sha256sum "$HSTATE/mux/hosts" | cut -d' ' -f1)" ] || {
284 echo "e2e FAIL: hosts: Ctrl-\\ c edited the hosts file — a birth is the daemon's, not the file's:" 349 echo "e2e FAIL: hosts: the wall edited the hosts file:"
285 cat "$HSTATE/mux/hosts"; exit 1; } 350 cat "$HSTATE/mux/hosts"; exit 1; }
286 HBORN=$(( $("$MUX" d stats --sock "$SOCKH1" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p') + 351 # ...and the layout it wrote back is the three leaves it was given.
287 $("$MUX" d stats --sock "$SOCKH2" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p') )) 352 _hw_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$HSTATE/mux/layout" | tr '\n' ' ')
288 [ "$HBORN" -eq 5 ] || { 353 [ "$_hw_leaves" = "--sock $SOCKH1#0 --sock $SOCKH1#b --sock $SOCKH2#0 " ] || {
289 echo "e2e FAIL: hosts: Ctrl-\\ c left $HBORN sessions across the two daemons, want 5" 354 echo "e2e FAIL: hosts: the wall saved leaves '$_hw_leaves', want the three it opened on:"
290 "$MUX" d stats --sock "$SOCKH1"; "$MUX" d stats --sock "$SOCKH2"; exit 1; } 355 cat "$HSTATE/mux/layout"; exit 1; }
291 # ...and a SECOND wall finds it, with nothing having been written down. 356
357 # A session born ELSEWHERE, while nothing was looking. Its own state home,
358 # so the birth writes no line into the wall this leg reads.
359 pipe_mux "$OUT.hd" "$OUT.hd.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 "$MUX" --sock "$SOCKH1" --session d
360 pipe_send 'printf "hd-%%s\\n" pin\n'
361 await_out "$OUT.hd" "hd-pin" "hd-pin never reached the client"
362 pipe_detach "hosts session d"
363 wait_grid "$SOCKH1" "hd-pin" "hosts: session d's marker" d
364 wait_sessions "$SOCKH1" 3 "hosts: daemon 1 should hold 0, b and d"
365
292 set +e 366 set +e
367 # A SECOND wall, opened after the birth: the file is the same three
368 # leaves, so the wall is the same three panes. Two and a half seconds of
369 # quiet is several poll answers on both daemons.
293 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \ 370 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \
294 --out "$OUT.hw2cap" --err "$OUT.hw2cap.err" -- "$MUX" > "$OUT.hw2pc" 2>&1 <<'EOF' 371 --out "$OUT.hw2cap" --err "$OUT.hw2cap.err" -- "$MUX" > "$OUT.hw2pc" 2>&1 <<'EOF'
295 expect [up] 25000 372 expect [up] 25000
296 settle 2000 25000 373 settle 2500 30000
297 send \x1cd 374 send \x1cd
298 waitexit 15000 375 waitexit 15000
299 EOF 376 EOF
300 RC=$? 377 RC=$?
301 set -e 378 set -e
302 rc0 "hosts: the second wall exited $RC:" "$OUT.hw2pc" 379 rc0 "hosts: the second wall exited $RC:" "$OUT.hw2pc"
303 # `1` is the name `nextFreeName` picks on either daemon — both hold `0` and 380 for _hn in "$SOCKH1#d" "$SOCKH2#c"; do
304 # one letter — so which host the birth landed on does not change what it is 381 [ "$(grep -aoF -- "$_hn" "$OUT.hw2cap" | wc -l)" -eq 0 ] || {
305 # called, only which stripe carries it. 382 echo "e2e FAIL: hosts: '$_hn' is a session no leaf names and it is on the"
306 grep -q -- "#1 \[" "$OUT.hw2cap" || { 383 echo " next wall — a birth anywhere still reaches every wall:"
307 echo "e2e FAIL: hosts: the session Ctrl-\\ c created is not a tile on the next wall:" 384 cat "$OUT.hw2pc"; exit 1; }
308 cat "$OUT.hw2pc"; exit 1; }
309 # ...and then it goes again, so the counts the legs below assert are the
310 # ones this file set up. WHICH of the two hosts the birth landed on depends
311 # on which polled first, and that is a race no leg here has any business
312 # pinning.
313 for _hs in "$SOCKH1" "$SOCKH2"; do
314 if "$MUX" d stats --sock "$_hs" | grep -q 'session 1 '; then
315 pipe_mux "$OUT.hkill" "$OUT.hkill.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 \
316 "$MUX" --sock "$_hs" --session 1
317 pipe_send 'exit 0\n'
318 pipe_waitexit "hosts: the born session"
319 fi
320 done 385 done
386 "$RENDER" --cols 80 --rows 44 < "$OUT.hw2cap" > "$OUT.hw2grid" || {
387 echo "e2e FAIL: hosts: render oracle failed"; cat "$OUT.hw2grid"; exit 1; }
388 [ "$(grep -c -- '--sock ' "$OUT.hw2grid")" = "3" ] || {
389 echo "e2e FAIL: hosts: the second wall's final screen does not hold exactly"
390 echo " three bars:"; cat "$OUT.hw2grid"; exit 1; }
391
392 # ...and then session d goes, so the counts the legs below assert are the
393 # ones this file set up.
394 pipe_mux "$OUT.hkill" "$OUT.hkill.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 \
395 "$MUX" --sock "$SOCKH1" --session d
396 pipe_send 'exit 0\n'
397 pipe_waitexit "hosts: the session born elsewhere"
321 wait_sessions "$SOCKH1" 2 "hosts: daemon 1 back to 0 and b" 398 wait_sessions "$SOCKH1" 2 "hosts: daemon 1 back to 0 and b"
322 wait_sessions "$SOCKH2" 2 "hosts: daemon 2 back to 0 and c" 399 wait_sessions "$SOCKH2" 2 "hosts: daemon 2 back to 0 and c"
323 ok "every live session of every listed daemon is a tile, n walks them, and a birth writes nothing down" 400 ok "a wall shows its layout's panes and no more; a session born elsewhere never appears"
324 401
325 # ---- Ctrl-\ x ends a session, and asks first ---------------------------- 402 # ---- the PICKER's x ends a session, and asks first -----------------------
326 # 403 #
327 # `x` is the one chord that destroys something, so it is the one chord that 404 # Ending a session is the picker's job now: `Ctrl-\ x` on the wall takes a
328 # refuses. Two clients on one session, and the three facts that make the 405 # pane off this terminal and leaves the shell running, so the one key that
329 # two-step a two-step rather than a delay: 406 # destroys something lives beside the count of who else is holding it.
407 # Two clients on one session, and the three facts that make the two-step a
408 # two-step rather than a delay:
330 # 409 #
331 # * the first `x` is REFUSED, by count, and the shell is still there — 410 # * the first `x` is REFUSED, by count, and the shell is still there —
332 # asked of the OS with `kill -0` on the pid the shell itself printed, 411 # asked of the OS with `kill -0` on the pid the shell itself printed,
333 # not of the daemon that would be reporting on its own refusal. 412 # not of the daemon that would be reporting on its own refusal. The
413 # count is TWO: `pickEnd` asks over a side connection on the poller's
414 # recipe, so the daemon counts every holder other than that connection
415 # — this wall's own pane for `xb` as much as the pipe client. Pinned as
416 # the number the user actually reads, and the fixture is built so both
417 # holders are real and distinguishable.
334 # * the second `x` ends it. The shell is gone from the process table. 418 # * the second `x` ends it. The shell is gone from the process table.
335 # * the OTHER client, which typed nothing at all, comes back with the 419 # * the OTHER client, which typed nothing at all, comes back with the
336 # session's exit status. A hangup is not a detach: it reaches everyone 420 # session's exit status. A hangup is not a detach: it reaches everyone
337 # attached. 421 # attached.
338 # 422 #
339 # Two ptyclient runs rather than one, because `kill -0` between the two 423 # Two ptyclient runs rather than one, because `kill -0` between the two
340 # chords is this script's to do and ptyclient's script cannot stop for it. 424 # presses is this script's to do and ptyclient's script cannot stop for it.
341 # The three-second latch means run B has to press `x` twice itself. 425 # The three-second latch is per CLIENT, so run B has to press `x` twice.
426 #
427 # The row is chosen by digit and the assertion NAMES the session: rows are
428 # the daemon's own list order (`0`, `b`, `xb`), so row 3 is `xb` — and a
429 # list that came back in another order fails on the notice's first word
430 # rather than ending somebody else's shell.
342 pipe_mux "$OUT.hxmk" "$OUT.hxmk.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 \ 431 pipe_mux "$OUT.hxmk" "$OUT.hxmk.err" env XDG_STATE_HOME="$HXSTATE" timeout 40 \
343 "$MUX" --sock "$SOCKH1" --session xb 432 "$MUX" --sock "$SOCKH1" --session xb
344 pipe_send 'echo xbpid=$$\n' 433 pipe_send 'echo xbpid=$$\n'
@@ -347,6 +436,10 @@ pipe_detach "hosts x setup"
347 wait_grid "$SOCKH1" "xbpid=" "hosts x: session xb's shell pid" xb 436 wait_grid "$SOCKH1" "xbpid=" "hosts x: session xb's shell pid" xb
348 XBPID=$(dump_session "$SOCKH1" xb | sed -n 's/.*xbpid=\([0-9]*\).*/\1/p' | tail -1) 437 XBPID=$(dump_session "$SOCKH1" xb | sed -n 's/.*xbpid=\([0-9]*\).*/\1/p' | tail -1)
349 [ -n "$XBPID" ] || { echo "e2e FAIL: hosts x: session xb printed no pid"; exit 1; } 438 [ -n "$XBPID" ] || { echo "e2e FAIL: hosts x: session xb printed no pid"; exit 1; }
439 XBEFORE=$("$MUX" d stats --sock "$SOCKH1" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p')
440 [ -n "$XBEFORE" ] || {
441 echo "e2e FAIL: hosts x: no sessions= in stats (the field moved?); it says:"
442 "$MUX" d stats --sock "$SOCKH1"; exit 1; }
350 443
351 # The other client: attached, silent, and holding its stdin open so nothing 444 # The other client: attached, silent, and holding its stdin open so nothing
352 # but the session's own end can bring it back. 445 # but the session's own end can bring it back.
@@ -360,18 +453,30 @@ XDG_STATE_HOME="$HXSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 30 \
360 "$MUX" --sock "$SOCKH1" --session xb > "$OUT.hx1pc" 2>&1 <<'EOF' 453 "$MUX" --sock "$SOCKH1" --session xb > "$OUT.hx1pc" 2>&1 <<'EOF'
361 expect xbpid= 20000 454 expect xbpid= 20000
362 settle 800 20000 455 settle 800 20000
363 send \x1cx 456 send \x1cs
457 expect x forget 15000
458 send 1
459 settle 400 15000
460 send \r
461 expect Enter add to wall 15000
462 send 3
463 settle 400 15000
464 send x
364 expect attached - x again to end 15000 465 expect attached - x again to end 15000
365 settle 600 15000 466 settle 600 15000
467 send \x1b
468 settle 300 15000
469 send \x1b
470 settle 400 15000
366 send \x1cd 471 send \x1cd
367 waitexit 10000 472 waitexit 10000
368 EOF 473 EOF
369 RC=$? 474 RC=$?
370 set -e 475 set -e
371 rc0 "hosts x: the refusal leg exited $RC (did the first x say who else was there?):" "$OUT.hx1pc" 476 rc0 "hosts x: the refusal leg exited $RC (did the first x say who else was there?):" "$OUT.hx1pc"
372 grep -qF -- "[1 other attached - x again to end]" "$OUT.hx1cap" || { 477 grep -qF -- "[xb: 2 others attached - x again to end]" "$OUT.hx1cap" || {
373 echo "e2e FAIL: hosts x: the refusal did not name the count:" 478 echo "e2e FAIL: hosts x: the refusal did not name the session and the count:"
374 grep -ao '\[[0-9]* other[^]]*\]' "$OUT.hx1cap" | head -3; exit 1; } 479 grep -ao '\[[^]]*other[^]]*\]' "$OUT.hx1cap" | head -3; exit 1; }
375 kill -0 "$XBPID" 2>/dev/null || { 480 kill -0 "$XBPID" 2>/dev/null || {
376 echo "e2e FAIL: hosts x: the FIRST x ended the shell despite another client"; exit 1; } 481 echo "e2e FAIL: hosts x: the FIRST x ended the shell despite another client"; exit 1; }
377 482
@@ -381,13 +486,25 @@ XDG_STATE_HOME="$HXSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 30 \
381 "$MUX" --sock "$SOCKH1" --session xb > "$OUT.hx2pc" 2>&1 <<'EOF' 486 "$MUX" --sock "$SOCKH1" --session xb > "$OUT.hx2pc" 2>&1 <<'EOF'
382 expect xbpid= 20000 487 expect xbpid= 20000
383 settle 800 20000 488 settle 800 20000
384 send \x1cx 489 send \x1cs
490 expect x forget 15000
491 send 1
492 settle 400 15000
493 send \r
494 expect Enter add to wall 15000
495 send 3
496 settle 400 15000
497 send x
385 expect attached - x again to end 15000 498 expect attached - x again to end 15000
386 settle 600 15000 499 settle 600 15000
387 send \x1cx 500 send x
388 settle 2000 20000 501 settle 2500 25000
502 send \x1b
503 settle 300 15000
504 send \x1b
505 settle 400 15000
389 send \x1cd 506 send \x1cd
390 waitexit 10000 507 waitexit 15000
391 EOF 508 EOF
392 RC=$? 509 RC=$?
393 set -e 510 set -e
@@ -399,61 +516,76 @@ wait_pid_gone "$XBPID" "hosts x: the shell after the second x"
399 # client noticing a closed socket. 516 # client noticing a closed socket.
400 pipe_waitexit "hosts x: the other client" 128 517 pipe_waitexit "hosts x: the other client" 128
401 wait_sessions "$SOCKH1" 2 "hosts x: only xb ended" 518 wait_sessions "$SOCKH1" 2 "hosts x: only xb ended"
402 ok "x refuses while others are attached, then ends; the other client sees the exit" 519 # ...and the daemon's own count went down by exactly one.
520 XAFTER=$("$MUX" d stats --sock "$SOCKH1" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p')
521 [ "$XAFTER" = "$(( XBEFORE - 1 ))" ] || {
522 echo "e2e FAIL: hosts x: daemon 1 holds $XAFTER sessions, want $(( XBEFORE - 1 ))"
523 "$MUX" d stats --sock "$SOCKH1"; exit 1; }
524 ok "the picker's x refuses while others hold the session, ends on the second press, and the other client sees the exit"
403 525
404 # ---- ...and on a WALL it ends the FOCUSED tile's session, only that one -- 526 # ---- ...and on a WALL, x removes the pane and ends NOTHING ---------------
527 #
528 # The other half of the split. `Ctrl-\ x` used to end the focused tile's
529 # session; it takes the pane off THIS wall now and leaves the session where
530 # it is, running, for whoever else holds it. The layout is the wall, so the
531 # removal is a leaf leaving a file — and nothing on any daemon changes.
405 # 532 #
406 # The leg above holds ONE tile: it can say the two-step is a two-step and 533 # Three panes, because a leg with one could not tell "the pane went" from
407 # nothing at all about WHICH session an `x` reaches. Five tiles over two 534 # "the wall went": the two neighbours have to still be there afterwards,
408 # daemons here — the four the legs above left, plus one this leg births 535 # and the two REMAINING leaves are what the detach writes.
409 # from the picker — and the `x` must end the born one and nothing else.
410 # 536 #
411 # The picker, not a digit, chooses the target. Digits are tile SLOTS, and 537 # Four oracles, none of them the wall's own word for it:
412 # the first cut fills them in the order the hosts' first lists LAND: one
413 # poller thread per host, and a local daemon's reply is a race the file
414 # order does not settle (seen: host 2's two sessions at digits 1-2, host
415 # 1's at 3-5 — the `x` aimed by digit at "the third session in file
416 # order" then ended daemon 1's own default session). The picker's rows ARE
417 # file order, so `1` there is daemon 1 whichever list won, and the tile it
418 # births is the focused one by construction.
419 # 538 #
420 # The `expect` on the born bar's `5>` is what pins the wall's shape: four 539 # * the screen: the removed pane's bar is gone from the final grid and
421 # tiles up before the birth, so the born one is the fifth and the `x` has 540 # its neighbours' are not (render, so a bar that merely scrolled past
422 # four neighbours to miss. `-seen` is in the printf's OUTPUT and not in 541 # in the stream does not count).
423 # the echoed command line, so the expect cannot be satisfied by the 542 # * the daemon: `mux d stats` still names the session, and says one
424 # keystrokes. `$$` is escaped for the heredoc, which is unquoted for 543 # client holds it — the pipe client below, which never typed a key.
425 # `$SOCKH1`; `\x1c` survives it, because a backslash in a heredoc is 544 # * that client: it takes a line and echoes it back, so it is attached
426 # special only before $, ` and a newline. 545 # and pumping rather than merely un-reaped.
546 # * the OS: the shell's own pid, printed by the shell itself before any
547 # of this, is still a live process.
427 # 548 #
428 # The shells are the OS's witnesses: every session's shell is a child of 549 # The layout is read from a copy a SURVIVING PANE'S SHELL makes while the
429 # its daemon, listed BEFORE the wall so the leg can ask afterwards whether 550 # wall is still on the screen, not off the disk afterwards. The run's exit
430 # each is still alive. A daemon reporting its own session table cannot 551 # saves the tree too, so a leg that read the file after the detach would
431 # catch itself ending the wrong shell. 552 # pass a `removePane` that wrote nothing at all — the removal's own save is
553 # what carries a wall that is killed rather than left.
432 wait_sessions "$SOCKH1" 2 "hosts wall x: daemon 1 should hold 0 and b" 554 wait_sessions "$SOCKH1" 2 "hosts wall x: daemon 1 should hold 0 and b"
433 wait_sessions "$SOCKH2" 2 "hosts wall x: daemon 2 should hold 0 and c" 555 wait_sessions "$SOCKH2" 2 "hosts wall x: daemon 2 should hold 0 and c"
434 XKEEP="$(ps -o pid= --ppid "$(real_pid "$DH1PID")") $(ps -o pid= --ppid "$(real_pid "$DH2PID")")" 556
435 [ "$(echo "$XKEEP" | wc -w)" -eq 4 ] || { 557 # The other holder of the pane's session, attached before the wall exists
436 echo "e2e FAIL: hosts wall x: expected four session shells under the two daemons, saw:" 558 # and silent until the removal is over.
437 echo "$XKEEP"; exit 1; } 559 pipe_mux "$OUT.hxb" "$OUT.hxb.err" env XDG_STATE_HOME="$HXSTATE" timeout 120 \
560 "$MUX" --sock "$SOCKH1" --session b
561 await_out "$OUT.hxb" "hb-pin" "hosts wall x: the other client never got session b's grid"
438 562
439 set +e 563 set +e
440 # A tree an earlier wall left here would be somebody else's shape. 564 # The same three leaves the headline leg opened on; pane 2 is
441 no_saved_tree "$HSTATE" 565 # `--sock $SOCKH1#b`, which is the one the other client is holding.
566 seed_layout "$HSTATE" stacked \
567 "--sock $SOCKH1#0" "--sock $SOCKH1#b" "--sock $SOCKH2#0"
568 rm -f "$OUT.hxlive"
442 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \ 569 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \
443 --out "$OUT.hxmcap" --err "$OUT.hxmcap.err" -- "$MUX" > "$OUT.hxmpc" 2>&1 <<EOF 570 --out "$OUT.hxmcap" --err "$OUT.hxmcap.err" -- "$MUX" > "$OUT.hxmpc" 2>&1 <<EOF
444 expect hw1-pin 25000 571 expect hw1-pin 25000
445 settle 1500 25000 572 settle 1500 25000
446 send \x1cs 573 send \x1c2
447 expect Enter/c new session 15000 574 settle 700 20000
448 send 1
449 settle 400 15000
450 send \r
451 expect 5> --sock $SOCKH1#1 [up] 25000
452 send printf 'xmpid=%s-%s\n' \$\$ seen\n
453 expect -seen 25000
454 send \x1cx 575 send \x1cx
455 expect [exited] 25000 576 settle 700 20000
577 # The sentence reaches the eyes that earned it on the next CLAIM, and
578 # `removePane` sets it AFTER the vanish that hands the focus on — so the
579 # incoming pump can take the terminal a beat before there is anything to
580 # say, and the notice then waits for a focus move that may never come
581 # (seen 2026-09-02, one run in four). `n` is that move, and it is what
582 # makes the sentence speakable rather than likely.
583 send \x1cn
584 expect the session is still on its daemon 20000
456 settle 2500 25000 585 settle 2500 25000
586 send cp $HSTATE/mux/layout $OUT.hxlive; printf 'xsn%s' ap; echo\n
587 expect xsnap 20000
588 settle 700 20000
457 send \x1cd 589 send \x1cd
458 waitexit 15000 590 waitexit 15000
459 EOF 591 EOF
@@ -462,36 +594,58 @@ set -e
462 [ "$RC" -eq 0 ] || { 594 [ "$RC" -eq 0 ] || {
463 echo "e2e FAIL: hosts wall x: the wall leg exited $RC:" 595 echo "e2e FAIL: hosts wall x: the wall leg exited $RC:"
464 cat "$OUT.hxmpc"; echo "--- stderr ---"; cat "$OUT.hxmcap.err"; exit 1; } 596 cat "$OUT.hxmpc"; echo "--- stderr ---"; cat "$OUT.hxmcap.err"; exit 1; }
465 XMPID=$(grep -ao 'xmpid=[0-9]*-seen' "$OUT.hxmcap" | head -1 | sed 's/xmpid=//; s/-seen//') 597 # The screen, judged on the GRID: the bar is in the stream either way, and
466 [ -n "$XMPID" ] || { echo "e2e FAIL: hosts wall x: the born session printed no pid"; exit 1; } 598 # only the render says what was still there when the wall came down.
467 # One press and not two, deliberately: nothing else holds the born
468 # session, so the daemon's refusal has nothing to count and the first
469 # press is the accepted one — the leg above owns the two-step.
470 wait_pid_gone "$XMPID" "hosts wall x: the focused tile's shell after x"
471 for _xk in $XKEEP; do
472 kill -0 "$_xk" 2>/dev/null || {
473 echo "e2e FAIL: hosts wall x: x ended the focused tile's session and ALSO"
474 echo " the shell $_xk of a neighbour:"
475 ps -o pid,ppid,args -p "$_xk" 2>&1; exit 1; }
476 done
477 wait_sessions "$SOCKH1" 2 "hosts wall x: daemon 1 back to 0 and b"
478 wait_sessions "$SOCKH2" 2 "hosts wall x: the other daemon must be untouched"
479 # ...and the tile left on the next LIST, judged on the grid: the bar is in
480 # the stream either way, and only the render says what was still on the
481 # screen when the wall came down.
482 "$RENDER" --cols 80 --rows 44 < "$OUT.hxmcap" > "$OUT.hxmgrid" || { 599 "$RENDER" --cols 80 --rows 44 < "$OUT.hxmcap" > "$OUT.hxmgrid" || {
483 echo "e2e FAIL: hosts wall x: render oracle failed"; cat "$OUT.hxmgrid"; exit 1; } 600 echo "e2e FAIL: hosts wall x: render oracle failed"; cat "$OUT.hxmgrid"; exit 1; }
484 grep -q -- "--sock $SOCKH1#1" "$OUT.hxmgrid" && { 601 grep -q -- "--sock $SOCKH1#b" "$OUT.hxmgrid" && {
485 echo "e2e FAIL: hosts wall x: the ended session still has a tile on the" 602 echo "e2e FAIL: hosts wall x: the removed pane is still on the final wall:"
486 echo " final wall — a tile leaves on the next list:"
487 cat "$OUT.hxmgrid"; exit 1; } 603 cat "$OUT.hxmgrid"; exit 1; }
488 for _hx in "$SOCKH1#0" "$SOCKH1#b" "$SOCKH2#0" "$SOCKH2#c"; do 604 for _hx in "$SOCKH1#0" "$SOCKH2#0"; do
489 grep -q -- "--sock $_hx" "$OUT.hxmgrid" || { 605 grep -q -- "--sock $_hx" "$OUT.hxmgrid" || {
490 echo "e2e FAIL: hosts wall x: '$_hx' lost its tile when its neighbour" 606 echo "e2e FAIL: hosts wall x: '$_hx' lost its pane when its neighbour"
491 echo " was ended:" 607 echo " was removed:"
492 cat "$OUT.hxmgrid"; exit 1; } 608 cat "$OUT.hxmgrid"; exit 1; }
493 done 609 done
494 ok "x on a wall ends the focused tile's session and no other, and that tile leaves the next list" 610 # The layout lost the leaf and kept the other two, in their order — read
611 # from a copy the SURVIVING PANE'S OWN SHELL made while the wall was still
612 # up, so the writer under test is `removePane`'s save and not the detach's.
613 # (Measured 2026-09-02: with `wall_layout.persist` dropped from `removePane`
614 # the file on disk after the detach is still right, because the run's exit
615 # saves the tree it has — so a leg that read it only afterwards would grade
616 # a removal that never wrote anything as green.)
617 [ -f "$OUT.hxlive" ] || {
618 echo "e2e FAIL: hosts wall x: the surviving pane never copied the live layout"
619 cat "$OUT.hxmpc"; exit 1; }
620 _hx_live=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$OUT.hxlive" | tr '\n' ' ')
621 [ "$_hx_live" = "--sock $SOCKH1#0 --sock $SOCKH2#0 " ] || {
622 echo "e2e FAIL: hosts wall x: the file the removal wrote is '$_hx_live',"
623 echo " want the two panes that stayed:"; cat "$OUT.hxlive"; exit 1; }
624 # ...and the detach agrees with it, so the two writers do not disagree.
625 _hx_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$HSTATE/mux/layout" | tr '\n' ' ')
626 [ "$_hx_leaves" = "$_hx_live" ] || {
627 echo "e2e FAIL: hosts wall x: the detach saved '$_hx_leaves' over the"
628 echo " removal's '$_hx_live':"; cat "$HSTATE/mux/layout"; exit 1; }
629 # The daemon still has the session, and one client — the pipe client — is
630 # still holding it. `x` is a pane leaving a file, not a word to a daemon.
631 wait_sessions "$SOCKH1" 2 "hosts wall x: daemon 1 must still hold 0 and b"
632 "$MUX" d stats --sock "$SOCKH1" > "$OUT.hxstats" 2>&1
633 grep -q "session b clients=1 " "$OUT.hxstats" || {
634 echo "e2e FAIL: hosts wall x: session b is gone, or its other client went with"
635 echo " the pane; the daemon says:"; cat "$OUT.hxstats"; exit 1; }
636 wait_sessions "$SOCKH2" 2 "hosts wall x: the other daemon must be untouched"
637 # ...and that client is still PUMPING, not merely counted: a line typed
638 # into it comes back off the session's own grid.
639 pipe_send 'printf "hxlive-%%s\\n" pin\n'
640 await_out "$OUT.hxb" "hxlive-pin" "hosts wall x: the other client stopped pumping when the pane went"
641 pipe_detach "hosts wall x: the other client"
642 # The OS's word on the shell, which no daemon can catch itself being wrong
643 # about.
644 kill -0 "$BSHELL" 2>/dev/null || {
645 echo "e2e FAIL: hosts wall x: session b's shell (pid $BSHELL) died when its pane"
646 echo " was taken off the wall — removing a pane is not ending a session"
647 ps -o pid,ppid,stat,args -p "$BSHELL" 2>&1; exit 1; }
648 ok "x on a wall removes the focused pane and ends nothing: the session keeps its other client, and the layout loses the leaf"
495 649
496 # ---- a daemon that goes and comes back re-creates NOTHING --------------- 650 # ---- a daemon that goes and comes back re-creates NOTHING ---------------
497 # 651 #
@@ -520,6 +674,11 @@ ok "x on a wall ends the focused tile's session and no other, and that tile leav
520 # session and the wall says so. 674 # session and the wall says so.
521 assert_stopped "$SOCKH2" "$DH2PID" "hosts restart" "$OUT.h2stop" 675 assert_stopped "$SOCKH2" "$DH2PID" "hosts restart" "$OUT.h2stop"
522 DH2PID="" 676 DH2PID=""
677 # Four panes, two per daemon, written down before the box goes dark: the
678 # claim is about panes a user SAVED on a machine that then stopped
679 # answering, so they have to be in the file rather than discovered.
680 seed_layout "$HSTATE" stacked \
681 "--sock $SOCKH1#0" "--sock $SOCKH1#b" "--sock $SOCKH2#0" "--sock $SOCKH2#c"
523 set +e 682 set +e
524 XDG_STATE_HOME="$HSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 44 \ 683 XDG_STATE_HOME="$HSTATE" timeout 60 "$PTYCLIENT" --cols 80 --rows 44 \
525 --out "$OUT.hdcap" --err "$OUT.hdcap.err" -- "$MUX" > "$OUT.hdpc" 2>&1 <<EOF 684 --out "$OUT.hdcap" --err "$OUT.hdcap.err" -- "$MUX" > "$OUT.hdpc" 2>&1 <<EOF
@@ -735,7 +894,7 @@ set +e
735 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \ 894 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \
736 --out "$OUT.emcap" --err "$OUT.emcap.err" -- "$MUX" > "$OUT.empc" 2>&1 <<'EOF' 895 --out "$OUT.emcap" --err "$OUT.emcap.err" -- "$MUX" > "$OUT.empc" 2>&1 <<'EOF'
737 expect the wall is empty 20000 896 expect the wall is empty 20000
738 expect Enter/c new session 20000 897 expect c new session 20000
739 send \x1b 898 send \x1b
740 settle 500 15000 899 settle 500 15000
741 send \x1cd 900 send \x1cd
@@ -762,7 +921,7 @@ EMLAST=$(grep -v '^[[:space:]]*$' "$OUT.emcap.err" | tail -1)
762 grep -q "the wall is empty" "$OUT.emgrid" || { 921 grep -q "the wall is empty" "$OUT.emgrid" || {
763 echo "e2e FAIL: empty wall: Esc did not leave the one-line text:" 922 echo "e2e FAIL: empty wall: Esc did not leave the one-line text:"
764 cat "$OUT.emgrid"; exit 1; } 923 cat "$OUT.emgrid"; exit 1; }
765 grep -q "Enter/c new session" "$OUT.emgrid" && { 924 grep -q "c new session" "$OUT.emgrid" && {
766 echo "e2e FAIL: empty wall: the picker came back over the answer:" 925 echo "e2e FAIL: empty wall: the picker came back over the answer:"
767 cat "$OUT.emgrid"; exit 1; } 926 cat "$OUT.emgrid"; exit 1; }
768 ok "an empty wall opens the picker, Esc leaves the one line, and Ctrl-\\ d leaves at once" 927 ok "an empty wall opens the picker, Esc leaves the one line, and Ctrl-\\ d leaves at once"
@@ -773,9 +932,13 @@ ok "an empty wall opens the picker, Esc leaves the one line, and Ctrl-\\ d leave
773 # happens here and nowhere else, so the four keys are asserted in one run 932 # happens here and nowhere else, so the four keys are asserted in one run
774 # and each against an oracle outside the terminal: 933 # and each against an oracle outside the terminal:
775 # 934 #
776 # * Enter births on the SELECTED host — the daemon's own session count, 935 # * `c` births on the SELECTED host — the daemon's own session count,
777 # read back with `mux d stats`, says which machine took it. A birth 936 # read back with `mux d stats`, says which machine took it. A birth
778 # driven off a tile could only ever have landed on the focus's daemon. 937 # driven off a tile could only ever have landed on the focus's daemon.
938 # * Enter on that host then LISTS its sessions, and the one just born
939 # wears `on this wall` beside the daemon's own count of who holds it.
940 # Esc backs out one level rather than closing, so the forget below is
941 # typed at the rows the Esc came back to.
779 # * `x` forgets a host: the file loses the line and the host's SHELL is 942 # * `x` forgets a host: the file loses the line and the host's SHELL is
780 # still there, asked of the OS with `kill -0` on the pid the shell 943 # still there, asked of the OS with `kill -0` on the pid the shell
781 # itself printed. "remove is detach" — `hosts rm` ends nothing. 944 # itself printed. "remove is detach" — `hosts rm` ends nothing.
@@ -825,7 +988,10 @@ wait_grid "$PKSOCK2" "pk-two-mark" "picker: daemon 2's marker"
825 988
826 mkdir -p "$PKSTATE/mux" 989 mkdir -p "$PKSTATE/mux"
827 printf -- '--sock %s\n--sock %s\n' "$PKSOCK1" "$PKSOCK2" > "$PKSTATE/mux/hosts" 990 printf -- '--sock %s\n--sock %s\n' "$PKSOCK1" "$PKSOCK2" > "$PKSTATE/mux/hosts"
828 no_saved_tree "$PKSTATE" 991 # One pane per daemon, so the popup opens over a wall rather than over the
992 # empty-wall line — and so the forget below has a pane of daemon 1's to
993 # take away.
994 seed_layout "$PKSTATE" stacked "--sock $PKSOCK1#0" "--sock $PKSOCK2#0"
829 995
830 PKBEFORE=$("$MUX" d stats --sock "$PKSOCK2" | tr ' ' '\n' | grep -c '^session$') 996 PKBEFORE=$("$MUX" d stats --sock "$PKSOCK2" | tr ' ' '\n' | grep -c '^session$')
831 set +e 997 set +e
@@ -835,20 +1001,27 @@ XDG_STATE_HOME="$PKSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \
835 expect pk-two-mark 20000 1001 expect pk-two-mark 20000
836 settle 700 20000 1002 settle 700 20000
837 send \x1cs 1003 send \x1cs
838 expect Enter/c new session 15000 1004 expect x forget 15000
839 send 2 1005 send 2
840 settle 400 15000 1006 settle 400 15000
841 send \r 1007 send c
842 expect 3> 20000 1008 expect 3> 20000
843 settle 700 20000 1009 settle 700 20000
844 send \x1cs 1010 send \x1cs
845 expect Enter/c new session 15000 1011 expect x forget 15000
1012 send 2
1013 settle 400 15000
1014 send \r
1015 expect on this wall, 1 client 15000
1016 settle 400 15000
1017 send \x1b
1018 expect x forget 15000
846 send 1 1019 send 1
847 settle 400 15000 1020 settle 400 15000
848 send x 1021 send x
849 settle 900 20000 1022 settle 900 20000
850 send \x1cs 1023 send \x1cs
851 expect Enter/c new session 15000 1024 expect x forget 15000
852 send a--sock\x20 1025 send a--sock\x20
853 settle 300 15000 1026 settle 300 15000
854 send $PKSOCK1\r 1027 send $PKSOCK1\r
@@ -912,7 +1085,121 @@ assert_stopped "$PKSOCK1" "$PK1PID" "picker" "$OUT.pk1stop"
912 PK1PID="" 1085 PK1PID=""
913 assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop" 1086 assert_stopped "$PKSOCK2" "$PK2PID" "picker" "$OUT.pk2stop"
914 PK2PID="" 1087 PK2PID=""
915 ok "the picker births on the host a digit names, forgets a host without ending it, adds one back, and gives the keyboard back" 1088 ok "the picker births on the host a digit names, lists its sessions, forgets a host without ending it, adds one back, and gives the keyboard back"
1089
1090 # ---- two devices, one pair of daemons: each wall is its own layout -------
1091 #
1092 # The whole point of the change, and the one thing no single-wall leg can
1093 # say. Two state homes stand in for two machines, both listing the SAME two
1094 # daemons. Device 1 has a wall of two panes; device 2 has no layout at all,
1095 # so it starts with nothing and gets exactly what it adds — and device 1
1096 # never learns of it.
1097 #
1098 # Under the old model this leg could not exist: tiles came from each host's
1099 # own list, so any two walls over one pair of daemons were the same wall,
1100 # and a session either of them started appeared on both.
1101 #
1102 # Daemon 2 holds TWO sessions on purpose. "Device 2 added ONE pane" is only
1103 # a claim if there was another it could have taken, and the row it picks is
1104 # the SECOND one — a first-row fixture would pass whatever the navigation
1105 # did.
1106 TWSOCK1="${TMPDIR:-/tmp}/muxd-e2e-twodev1-$$.sock"
1107 defer_sock "$TWSOCK1"
1108 TWSOCK2="${TMPDIR:-/tmp}/muxd-e2e-twodev2-$$.sock"
1109 defer_sock "$TWSOCK2"
1110 TWDEV1="${TMPDIR:-/tmp}/mux-e2e-twodev-a-$$"
1111 defer_rm "$TWDEV1"
1112 TWDEV2="${TMPDIR:-/tmp}/mux-e2e-twodev-b-$$"
1113 defer_rm "$TWDEV2"
1114 start_daemon "$TWSOCK1" "$OUT.tw1.d" "two-device daemon 1 never bound" --shell /bin/sh
1115 TW1PID=$DPID
1116 start_daemon "$TWSOCK2" "$OUT.tw2.d" "two-device daemon 2 never bound" --shell /bin/sh
1117 TW2PID=$DPID
1118
1119 # Three markers, each printed by its own shell before either wall exists:
1120 # what proves a pane is a real attach and not an echo of anything typed.
1121 TWMKSTATE="${TMPDIR:-/tmp}/mux-e2e-twodev-mk-$$"
1122 defer_rm "$TWMKSTATE"
1123 for _tw in "$TWSOCK1:0:alpha" "$TWSOCK2:0:bee" "$TWSOCK2:beta:beta"; do
1124 _twsock=${_tw%%:*}; _twrest=${_tw#*:}; _twsess=${_twrest%%:*}; _twmark=${_twrest##*:}
1125 pipe_mux "$OUT.tw.$_twmark" "$OUT.tw.$_twmark.err" env XDG_STATE_HOME="$TWMKSTATE" \
1126 timeout 40 "$MUX" --sock "$_twsock" --session "$_twsess"
1127 pipe_send 'printf "tw-%%s\\n" '"$_twmark"'\n'
1128 await_out "$OUT.tw.$_twmark" "tw-$_twmark" "two devices: $_twmark never reached its client"
1129 pipe_detach "two devices: the $_twmark maker"
1130 wait_grid "$_twsock" "tw-$_twmark" "two devices: $_twmark's marker" "$_twsess"
1131 done
1132 wait_sessions "$TWSOCK2" 2 "two devices: daemon 2 should hold 0 and beta"
1133
1134 # Device 1: both daemons listed, and a wall of two panes over them.
1135 mkdir -p "$TWDEV1/mux"
1136 printf -- '--sock %s\n--sock %s\n' "$TWSOCK1" "$TWSOCK2" > "$TWDEV1/mux/hosts"
1137 seed_layout "$TWDEV1" stacked "--sock $TWSOCK1#0" "--sock $TWSOCK2#0"
1138 set +e
1139 XDG_STATE_HOME="$TWDEV1" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
1140 --out "$OUT.tw1cap" --err "$OUT.tw1cap.err" -- "$MUX" > "$OUT.tw1pc" 2>&1 <<'EOF'
1141 expect tw-alpha 25000
1142 settle 1500 25000
1143 send \x1cd
1144 waitexit 15000
1145 EOF
1146 RC=$?
1147 set -e
1148 rc0 "two devices: device 1's wall exited $RC:" "$OUT.tw1pc" "$OUT.tw1cap.err"
1149
1150 # Device 2: the same two daemons, no layout. `mux` opens on an empty wall
1151 # with the picker over it; Enter on host 2 lists its sessions, `j` steps to
1152 # the second, and Enter makes it a pane. Nothing else arrives.
1153 mkdir -p "$TWDEV2/mux"
1154 printf -- '--sock %s\n--sock %s\n' "$TWSOCK1" "$TWSOCK2" > "$TWDEV2/mux/hosts"
1155 [ ! -e "$TWDEV2/mux/layout" ] || {
1156 echo "e2e FAIL: two devices: device 2 already has a layout"; exit 1; }
1157 set +e
1158 XDG_STATE_HOME="$TWDEV2" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
1159 --out "$OUT.tw2cap" --err "$OUT.tw2cap.err" -- "$MUX" > "$OUT.tw2pc" 2>&1 <<'EOF'
1160 expect the wall is empty 20000
1161 expect x forget 20000
1162 send 2
1163 settle 400 15000
1164 send \r
1165 expect Enter add to wall 15000
1166 send j
1167 settle 300 15000
1168 send \r
1169 expect tw-beta 25000
1170 settle 1500 20000
1171 send \x1cd
1172 waitexit 15000
1173 EOF
1174 RC=$?
1175 set -e
1176 rc0 "two devices: device 2's wall exited $RC:" "$OUT.tw2pc" "$OUT.tw2cap.err"
1177 # One pane, the one it added.
1178 _tw2_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$TWDEV2/mux/layout" | tr '\n' ' ')
1179 [ "$_tw2_leaves" = "--sock $TWSOCK2#beta " ] || {
1180 echo "e2e FAIL: two devices: device 2's wall is '$_tw2_leaves', want the one pane it added:"
1181 cat "$TWDEV2/mux/layout"; exit 1; }
1182 # ...and nothing device 1 has. Both `#0` sessions were live on both listed
1183 # daemons for the whole run, and device 2 saw neither.
1184 [ "$(grep -aoF "#0" "$OUT.tw2cap" | wc -l)" -eq 0 ] || {
1185 echo "e2e FAIL: two devices: device 2 saw a pane it never added:"
1186 cat "$OUT.tw2pc"; exit 1; }
1187 for _twm in tw-alpha tw-bee; do
1188 [ "$(grep -aoF "$_twm" "$OUT.tw2cap" | wc -l)" -eq 0 ] || {
1189 echo "e2e FAIL: two devices: device 1's session $_twm painted on device 2:"
1190 cat "$OUT.tw2pc"; exit 1; }
1191 done
1192 # Device 1 is unchanged: its file still holds the two panes it was given,
1193 # and nothing device 2 added.
1194 _tw1_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$TWDEV1/mux/layout" | tr '\n' ' ')
1195 [ "$_tw1_leaves" = "--sock $TWSOCK1#0 --sock $TWSOCK2#0 " ] || {
1196 echo "e2e FAIL: two devices: device 1's wall changed under device 2's add: '$_tw1_leaves'"
1197 cat "$TWDEV1/mux/layout"; exit 1; }
1198 assert_stopped "$TWSOCK1" "$TW1PID" "two devices" "$OUT.tw1stop"
1199 TW1PID=""
1200 assert_stopped "$TWSOCK2" "$TW2PID" "two devices" "$OUT.tw2stop"
1201 TW2PID=""
1202 ok "two walls on the same daemons are two layouts; neither learns of the other's panes"
916 1203
917 # ---- reading a host never starts a daemon; asking does ------------------ 1204 # ---- reading a host never starts a daemon; asking does ------------------
918 # 1205 #
@@ -922,7 +1209,7 @@ ok "the picker births on the host a digit names, forgets a host without ending i
922 # READ, and a `mux d stop` typed on that box was undone a second later. 1209 # READ, and a `mux d stop` typed on that box was undone a second later.
923 # 1210 #
924 # The rule now: a daemon starts when somebody asks, and the ask is a WORD. 1211 # The rule now: a daemon starts when somebody asks, and the ask is a WORD.
925 # `mux HOST` and the picker's Enter spell `mux d endpoint --start`, which 1212 # `mux HOST` and the picker's `c` spell `mux d endpoint --start`, which
926 # ensures a daemon and announces in the same run; the poller, `mux hosts` 1213 # ensures a daemon and announces in the same run; the poller, `mux hosts`
927 # and every redial spell the bare verb, which can start nothing whatever 1214 # and every redial spell the bare verb, which can start nothing whatever
928 # the client does with the answer. 1215 # the client does with the answer.
@@ -1025,11 +1312,12 @@ NSTARTS=$(grep -c -- '--start' "$NSHIMLOG" || true)
1025 echo "e2e FAIL: no-start: mux hosts spelled the asking word $NSTARTS times:" 1312 echo "e2e FAIL: no-start: mux hosts spelled the asking word $NSTARTS times:"
1026 cat "$NSHIMLOG"; exit 1; } 1313 cat "$NSHIMLOG"; exit 1; }
1027 1314
1028 # (2) and (3) in one wall. The wall polls both boxes once a second — three 1315 # (2) and (3) in one wall. The wall has no layout, so it opens empty with
1029 # seconds of that must change nothing — and then Enter on ROW 2 asks, which 1316 # the picker over it — which is where the keystrokes below go. It polls
1030 # must give box B a daemon and box A none. Row 2 and not row 1: a birth 1317 # both boxes once a second — three seconds of that must change nothing —
1031 # that reached for the first row, or for the focus's host, would pass a 1318 # and then `c` on ROW 2 asks, which must give box B a daemon and box A
1032 # one-row fixture and this one catches it. 1319 # none. Row 2 and not row 1: a birth that reached for the first row, or for
1320 # the focus's host, would pass a one-row fixture and this one catches it.
1033 set +e 1321 set +e
1034 SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 90 "$PTYCLIENT" \ 1322 SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 90 "$PTYCLIENT" \
1035 --cols 100 --rows 30 --out "$OUT.nscap" --err "$OUT.nscap.err" -- \ 1323 --cols 100 --rows 30 --out "$OUT.nscap" --err "$OUT.nscap.err" -- \
@@ -1038,7 +1326,7 @@ expect unreachable 25000
1038 settle 3000 25000 1326 settle 3000 25000
1039 send 2 1327 send 2
1040 settle 500 15000 1328 settle 500 15000
1041 send \r 1329 send c
1042 settle 2000 30000 1330 settle 2000 30000
1043 send printf 'ns-born-%s\n' ok\n 1331 send printf 'ns-born-%s\n' ok\n
1044 expect ns-born-ok 25000 1332 expect ns-born-ok 25000
@@ -1056,19 +1344,19 @@ nostart_empty "$NRUNA" "$NSOCKA" "a wall polling it"
1056 # every poll cycle, would show as several. 1344 # every poll cycle, would show as several.
1057 NCB=$(nostart_daemons "$NRUNB") 1345 NCB=$(nostart_daemons "$NRUNB")
1058 [ "$NCB" -eq 1 ] || { 1346 [ "$NCB" -eq 1 ] || {
1059 echo "e2e FAIL: no-start: picker Enter left $NCB mux d start under $NRUNB, want 1"; exit 1; } 1347 echo "e2e FAIL: no-start: the picker birth left $NCB mux d start under $NRUNB, want 1"; exit 1; }
1060 [ -S "$NSOCKB" ] || { 1348 [ -S "$NSOCKB" ] || {
1061 echo "e2e FAIL: no-start: picker Enter started nothing on box B ($NSOCKB)"; exit 1; } 1349 echo "e2e FAIL: no-start: the picker birth started nothing on box B ($NSOCKB)"; exit 1; }
1062 # ONE ask in the whole run, and it is box B's. Three seconds of polling 1350 # ONE ask in the whole run, and it is box B's. Three seconds of polling
1063 # both boxes ran the reading word many times over and the asking word 1351 # both boxes ran the reading word many times over and the asking word
1064 # never; the Enter ran it once. A poll that had inherited the flag would 1352 # never; the `c` ran it once. A poll that had inherited the flag would
1065 # show a count here in the tens, and box A would not be empty above. 1353 # show a count here in the tens, and box A would not be empty above.
1066 NASKS=$(grep -c -- '--start' "$NSHIMLOG" || true) 1354 NASKS=$(grep -c -- '--start' "$NSHIMLOG" || true)
1067 [ "$NASKS" -eq 1 ] || { 1355 [ "$NASKS" -eq 1 ] || {
1068 echo "e2e FAIL: no-start: the picker leg spelled the asking word $NASKS times, want 1" 1356 echo "e2e FAIL: no-start: the picker leg spelled the asking word $NASKS times, want 1"
1069 grep -- '--start' "$NSHIMLOG"; exit 1; } 1357 grep -- '--start' "$NSHIMLOG"; exit 1; }
1070 grep -q -- 'nostart-b@.*--start' "$NSHIMLOG" || { 1358 grep -q -- 'nostart-b@.*--start' "$NSHIMLOG" || {
1071 echo "e2e FAIL: no-start: the one ask did not go to box B, the row Enter chose:" 1359 echo "e2e FAIL: no-start: the one ask did not go to box B, the row c chose:"
1072 grep -- '--start' "$NSHIMLOG"; exit 1; } 1360 grep -- '--start' "$NSHIMLOG"; exit 1; }
1073 # ...and it is a real session, named `0` off the daemon's own empty list. 1361 # ...and it is a real session, named `0` off the daemon's own empty list.
1074 dump_session "$NSOCKB" 0 | grep -q 'ns-born-ok' || { 1362 dump_session "$NSOCKB" 0 | grep -q 'ns-born-ok' || {
@@ -1095,10 +1383,10 @@ SHELL=/bin/sh XDG_STATE_HOME="$NSTATE" PATH="$NPATH" timeout 90 "$PTYCLIENT" \
1095 "$MUX" > "$OUT.nspc2" 2>&1 <<EOF 1383 "$MUX" > "$OUT.nspc2" 2>&1 <<EOF
1096 expect ns-born-ok 25000 1384 expect ns-born-ok 25000
1097 send \x1cs 1385 send \x1cs
1098 expect Enter/c new session 15000 1386 expect x forget 15000
1099 send 2 1387 send 2
1100 settle 500 15000 1388 settle 500 15000
1101 send \r 1389 send c
1102 settle 2000 30000 1390 settle 2000 30000
1103 send printf 'ns-two-%s\n' ok\n 1391 send printf 'ns-two-%s\n' ok\n
1104 expect ns-two-ok 25000 1392 expect ns-two-ok 25000
@@ -1166,7 +1454,7 @@ NASKS_ALL=$(grep -c -- '--start' "$NSHIMLOG" || true)
1166 echo "e2e FAIL: no-start: mux d stop on box A exited nonzero:" 1454 echo "e2e FAIL: no-start: mux d stop on box A exited nonzero:"
1167 cat "$OUT.nsastop"; exit 1; } 1455 cat "$OUT.nsastop"; exit 1; }
1168 nostart_empty "$NRUNA" "$NSOCKA" "mux d stop" 1456 nostart_empty "$NRUNA" "$NSOCKA" "mux d stop"
1169 ok "reading a host leaves it alone; a picker Enter and a cold mux HOST each start one daemon" 1457 ok "reading a host leaves it alone; a picker birth and a cold mux HOST each start one daemon"
1170 1458
1171 # ---- ssh's stderr is mux's: no byte of it reaches the wall -------------- 1459 # ---- ssh's stderr is mux's: no byte of it reaches the wall --------------
1172 # 1460 #
test/e2e_10_agent.sh
Old New
@@ -106,7 +106,7 @@ send exit\n
106 # is what makes this leg fail if the exit ever ends mux again instead. The 106 # is what makes this leg fail if the exit ever ends mux again instead. The
107 # empty-wall scenario asserts a picker on a wall that never had a tile; 107 # empty-wall scenario asserts a picker on a wall that never had a tile;
108 # only this one sees the TRANSITION. 108 # only this one sees the TRANSITION.
109 expect Enter/c new session 15000 109 expect c new session 15000
110 send \x1b 110 send \x1b
111 settle 300 15000 111 settle 300 15000
112 send \x1cd 112 send \x1cd
@@ -210,6 +210,11 @@ D42PID=$DPID
210 AR0=$(date +%s%N) 210 AR0=$(date +%s%N)
211 set +e 211 set +e
212 hostroom agtn 212 hostroom agtn
213 # The neighbour, written down: the wall is the layout, so the daemon's own
214 # `0` is a pane only if a leaf names it. Without it the `exit` below leaves
215 # an EMPTY wall and the picker over it, and the `\x1cd` that follows would
216 # be eaten by the popup.
217 seed_layout "$HOSTROOM" stacked "--sock $SOCK48#0"
213 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \ 218 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \
214 -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtn.log" 2>&1 <<'EOF' 219 -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtn.log" 2>&1 <<'EOF'
215 expect \x1b[?1049h 15000 220 expect \x1b[?1049h 15000
@@ -298,6 +303,8 @@ grep -qE "agtrc=[1-9]" "$OUT.agtn" || {
298 # expanded by THIS shell, so `$?` is escaped to reach the session's. 303 # expanded by THIS shell, so `$?` is escaped to reach the session's.
299 set +e 304 set +e
300 hostroom agtnest 305 hostroom agtnest
306 # The neighbour, for the leg above's reason.
307 seed_layout "$HOSTROOM" stacked "--sock $SOCK48#0"
301 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtnest" --err "$OUT.agtnest.err" \ 308 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtnest" --err "$OUT.agtnest.err" \
302 -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtnest.log" 2>&1 <<EOF 309 -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtnest.log" 2>&1 <<EOF
303 expect \x1b[?1049h 15000 310 expect \x1b[?1049h 15000
@@ -438,7 +445,7 @@ send exit\n
438 # settle and the `\x1b` would eat the `d` that follows (every byte is the 445 # settle and the `\x1b` would eat the `d` that follows (every byte is the
439 # popup's while it is up) and this leg would fail at waitexit on a change 446 # popup's while it is up) and this leg would fail at waitexit on a change
440 # nobody made. 447 # nobody made.
441 expect Enter/c new session 20000 448 expect c new session 20000
442 send \x1b 449 send \x1b
443 settle 300 20000 450 settle 300 20000
444 send \x1cd 451 send \x1cd
@@ -465,7 +472,7 @@ expect $FPB 20000
465 # legend and not a settle, doubly so here: B is a passive observer, so its 472 # legend and not a settle, doubly so here: B is a passive observer, so its
466 # screen is already quiet when A types `exit` and a duration could expire 473 # screen is already quiet when A types `exit` and a duration could expire
467 # before the transition this waits for has happened at all. 474 # before the transition this waits for has happened at all.
468 expect Enter/c new session 20000 475 expect c new session 20000
469 send \x1b 476 send \x1b
470 settle 300 20000 477 settle 300 20000
471 send \x1cd 478 send \x1cd
test/e2e_11_select.sh
Old New
@@ -22,13 +22,14 @@ defer_rm "$SELSTATE"
22 22
23 # ---- the SOCK50 cluster: a daemon, two sessions ------------------------ 23 # ---- the SOCK50 cluster: a daemon, two sessions ------------------------
24 # 24 #
25 # A daemon and two sessions — its own default one and a named `b` — 25 # A daemon and two sessions — its own default one and a named `b` — and a
26 # shared by the click legs below. A daemon is born holding `0`, so a wall 26 # two-leaf layout per run, because the wall is the file: the cut these legs
27 # over it always has that tile; naming a second would make these 27 # click into is authored, and the ORIENTATION is authored with it (the
28 # two-tile legs three-tile ones. The wall-mouse filter this cluster used 28 # aspect rule only ever applied to a wall with no file). The wall-mouse
29 # to open with is gone — every click and drag routes to the focused tile's 29 # filter this cluster used to open with is gone — every click and drag
30 # own Core, hit-tested by rect — so what remains here is the setup: a 30 # routes to the focused tile's own Core, hit-tested by rect — so what
31 # daemon, a marker per session, and the tiles `mux --sock` over it paints. 31 # remains here is the setup: a daemon, a marker per session, and the panes
32 # each run writes down before it opens.
32 start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh 33 start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh
33 D48PID=$DPID 34 D48PID=$DPID
34 start_daemon "$SOCKSEL" "$OUT.sel.d" "selection daemon never bound" --shell /bin/sh 35 start_daemon "$SOCKSEL" "$OUT.sel.d" "selection daemon never bound" --shell /bin/sh
@@ -56,7 +57,7 @@ wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b
56 # other way. A negative that cannot fail is not a test, so each carries the 57 # other way. A negative that cannot fail is not a test, so each carries the
57 # marker that MUST land in the focused session. 58 # marker that MUST land in the focused session.
58 set +e 59 set +e
59 no_saved_tree "$WMSTATE" 60 seed_layout "$WMSTATE" stacked "--sock $SOCK50#0" "--sock $SOCK50#b"
60 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ 61 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
61 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \ 62 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \
62 "$MUX" --sock "$SOCK50" > "$OUT.wmpc3" 2>&1 <<'EOF' 63 "$MUX" --sock "$SOCK50" > "$OUT.wmpc3" 2>&1 <<'EOF'
@@ -85,7 +86,7 @@ grep -q "wm-three" "$OUT.wmfa" && {
85 # The other direction: focus b explicitly, then click a's content and watch 86 # The other direction: focus b explicitly, then click a's content and watch
86 # the focus come back to a. 87 # the focus come back to a.
87 set +e 88 set +e
88 no_saved_tree "$WMSTATE" 89 seed_layout "$WMSTATE" stacked "--sock $SOCK50#0" "--sock $SOCK50#b"
89 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ 90 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \
90 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \ 91 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \
91 "$MUX" --sock "$SOCK50" > "$OUT.wmpc4" 2>&1 <<'EOF' 92 "$MUX" --sock "$SOCK50" > "$OUT.wmpc4" 2>&1 <<'EOF'
@@ -120,7 +121,7 @@ ok "a click in a tile's rect focuses it, from wherever the focus was"
120 # test in rectHit earns its keep: focus starts on a (the left pane) and 121 # test in rectHit earns its keep: focus starts on a (the left pane) and
121 # the click must move it to b (the right pane). 122 # the click must move it to b (the right pane).
122 set +e 123 set +e
123 no_saved_tree "$WMSTATE" 124 seed_layout "$WMSTATE" beside "--sock $SOCK50#0" "--sock $SOCK50#b"
124 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ 125 XDG_STATE_HOME="$WMSTATE" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \
125 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \ 126 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \
126 "$MUX" --sock "$SOCK50" > "$OUT.wmpc5" 2>&1 <<'EOF' 127 "$MUX" --sock "$SOCK50" > "$OUT.wmpc5" 2>&1 <<'EOF'
@@ -389,7 +390,7 @@ send exit\n
389 # what this waits for — a settle measures silence, and the picker opens on 390 # what this waits for — a settle measures silence, and the picker opens on
390 # the host poll, so a duration can expire in the window where the popup is 391 # the host poll, so a duration can expire in the window where the popup is
391 # about to eat the `d`. 392 # about to eat the `d`.
392 expect Enter/c new session 15000 393 expect c new session 15000
393 send \x1b 394 send \x1b
394 settle 300 15000 395 settle 300 15000
395 send \x1cd 396 send \x1cd
test/e2e_12_panes.sh
Old New
@@ -57,11 +57,12 @@ defer_rm "$CUSTATE"
57 # deterministic, because every paint now ends at the focused tile's cursor. 57 # deterministic, because every paint now ends at the focused tile's cursor.
58 # 58 #
59 # Three sessions on a daemon of its own — the daemon's own `0` and two 59 # Three sessions on a daemon of its own — the daemon's own `0` and two
60 # named ones, because a daemon is born holding `0` and a wall over it 60 # named ones — each marked by its shell before the wall attaches (the
61 # 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, 61 # shell-expanded marker trick: a hit is the shell's work, never an echo of
62 # never an echo of anything typed here). The ptyclient wall focuses tile 3, 62 # anything typed here), and all three written into a layout, because the
63 # settles, and detaches; the capture's last cursor-position escape is the 63 # file is the wall and a session no leaf names gets no stripe. The
64 # witness. 64 # ptyclient wall focuses tile 3, settles, and detaches; the capture's last
65 # cursor-position escape is the witness.
65 start_daemon "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound" --shell /bin/sh 66 start_daemon "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound" --shell /bin/sh
66 D52PID=$DPID 67 D52PID=$DPID
67 68
@@ -82,7 +83,7 @@ pipe_detach
82 wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c 83 wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c
83 84
84 set +e 85 set +e
85 no_saved_tree "$CUSTATE" 86 seed_layout "$CUSTATE" stacked "--sock $SOCK52#0" "--sock $SOCK52#b" "--sock $SOCK52#c"
86 XDG_STATE_HOME="$CUSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \ 87 XDG_STATE_HOME="$CUSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \
87 "$MUX" --sock "$SOCK52" > "$OUT.cupc" 2>&1 <<'EOF' 88 "$MUX" --sock "$SOCK52" > "$OUT.cupc" 2>&1 <<'EOF'
88 expect mc-pin 20000 89 expect mc-pin 20000
@@ -96,13 +97,12 @@ RC=$?
96 set -e 97 set -e
97 rc0 "cursor-ownership: ptyclient leg exited $RC:" "$OUT.cupc" "$OUT.cucap.err" 98 rc0 "cursor-ownership: ptyclient leg exited $RC:" "$OUT.cupc" "$OUT.cucap.err"
98 # WHICH rows tile 3's stripe owns is read off the capture rather than 99 # WHICH rows tile 3's stripe owns is read off the capture rather than
99 # counted. A host's list arrives in the daemon's slot order and is laid out 100 # counted. The layout's leaves are seated in file order, so the third digit
100 # in that order, so the third digit IS the third stripe down — but that is 101 # IS the third stripe down — but that is `seedLayout`'s claim, not this
101 # the birth-order leg's claim (`applyHostList: one list's tiles are laid out 102 # one's, and a leg that hard-codes rows 21-30 fails for whichever reason
102 # in the order the daemon reported them`), not this one's, and a leg that 103 # comes first. Tile 3's own label bar's CUP row is the top of its stripe,
103 # hard-codes rows 21-30 fails for whichever reason comes first. Tile 3's own 104 # and the nine rows under that are its content (30 rows over three tiles,
104 # label bar's CUP row is the top of its stripe, and the nine rows under that 105 # one of each ten on the bar).
105 # are its content (30 rows over three tiles, one of each ten on the bar).
106 _bar3=$(grep -ao $'\x1b\\[[0-9][0-9]*;1H\x1b\\[7m 3> ' "$OUT.cucap" | tail -1) 106 _bar3=$(grep -ao $'\x1b\\[[0-9][0-9]*;1H\x1b\\[7m 3> ' "$OUT.cucap" | tail -1)
107 _bar_row=${_bar3#??} 107 _bar_row=${_bar3#??}
108 _bar_row=${_bar_row%%;*} 108 _bar_row=${_bar_row%%;*}
@@ -169,7 +169,7 @@ pipe_detach
169 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b 169 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b
170 170
171 set +e 171 set +e
172 no_saved_tree "$SB2STATE" 172 seed_layout "$SB2STATE" stacked "--sock $SOCK53#0" "--sock $SOCK53#b"
173 XDG_STATE_HOME="$SB2STATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \ 173 XDG_STATE_HOME="$SB2STATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \
174 "$MUX" --sock "$SOCK53" > "$OUT.sb2pc" 2>&1 <<'EOF' 174 "$MUX" --sock "$SOCK53" > "$OUT.sb2pc" 2>&1 <<'EOF'
175 expect sb2nbr 20000 175 expect sb2nbr 20000
@@ -226,7 +226,7 @@ wait_grid "$SOCK54" "fs-init-b" "fullscreen: session b's marker" b
226 226
227 set +e 227 set +e
228 # wide: 80x24 trips the aspect rule (80 >= 48 → beside) 228 # wide: 80x24 trips the aspect rule (80 >= 48 → beside)
229 no_saved_tree "$FSSTATE" 229 seed_layout "$FSSTATE" beside "--sock $SOCK54#0" "--sock $SOCK54#b"
230 XDG_STATE_HOME="$FSSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.fscap" --err "$OUT.fscap.err" -- \ 230 XDG_STATE_HOME="$FSSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.fscap" --err "$OUT.fscap.err" -- \
231 "$MUX" --sock "$SOCK54" > "$OUT.fspc" 2>&1 <<'EOF' 231 "$MUX" --sock "$SOCK54" > "$OUT.fspc" 2>&1 <<'EOF'
232 expect fs-init-b 20000 232 expect fs-init-b 20000
@@ -312,7 +312,7 @@ set +e
312 # 80x24: aspect rule makes beside; two panes split 39/40 with the rail 312 # 80x24: aspect rule makes beside; two panes split 39/40 with the rail
313 # at column 40 (1-indexed). Three l presses grow the left pane by 3, 313 # at column 40 (1-indexed). Three l presses grow the left pane by 3,
314 # moving the rail to column 43. 314 # moving the rail to column 43.
315 no_saved_tree "$RSZSTATE" 315 seed_layout "$RSZSTATE" beside "--sock $SOCK55#0" "--sock $SOCK55#b"
316 XDG_STATE_HOME="$RSZSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.rszcap" --err "$OUT.rszcap.err" -- \ 316 XDG_STATE_HOME="$RSZSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.rszcap" --err "$OUT.rszcap.err" -- \
317 "$MUX" --sock "$SOCK55" > "$OUT.rszpc" 2>&1 <<'EOF' 317 "$MUX" --sock "$SOCK55" > "$OUT.rszpc" 2>&1 <<'EOF'
318 expect rsz-init-b 20000 318 expect rsz-init-b 20000
@@ -386,7 +386,7 @@ wait_grid "$SOCK56" "spc-right-survivor" "span-clear: right session's marker" b
386 set +e 386 set +e
387 # 80x24: aspect rule → beside. The wall client is a tty session: its 387 # 80x24: aspect rule → beside. The wall client is a tty session: its
388 # stdout is the terminal's full byte stream, captured by ptyclient. 388 # stdout is the terminal's full byte stream, captured by ptyclient.
389 no_saved_tree "$SPCSTATE" 389 seed_layout "$SPCSTATE" beside "--sock $SOCK56#0" "--sock $SOCK56#b"
390 XDG_STATE_HOME="$SPCSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.spccap" --err "$OUT.spccap.err" -- \ 390 XDG_STATE_HOME="$SPCSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.spccap" --err "$OUT.spccap.err" -- \
391 "$MUX" --sock "$SOCK56" > "$OUT.spcpc" 2>&1 <<'EOF' 391 "$MUX" --sock "$SOCK56" > "$OUT.spcpc" 2>&1 <<'EOF'
392 expect spc-right-survivor 20000 392 expect spc-right-survivor 20000
@@ -544,11 +544,11 @@ ok "Ctrl-\\ | births a session beside; its label bar sits right of the rail"
544 544
545 # ---- layout sidecar: restore round-trip ------------------------------ 545 # ---- layout sidecar: restore round-trip ------------------------------
546 # 546 #
547 # A no-argv `mux` opens the wall of hosts, and on Ctrl-\ d the resized tree 547 # A no-argv `mux` opens the layout, and on Ctrl-\ d the resized tree is
548 # is saved to the layout sidecar. Reattaching with the same state home 548 # saved back to it. Reattaching with the same state home loads it verbatim
549 # loads the sidecar verbatim — the resize survived the round trip. The rail's CUP column in the second capture is >= 43 (three 549 # — the resize survived the round trip. The rail's CUP column in the second
550 # right-resizes from a 40|40 split), and a marker typed into the restored 550 # capture is >= 43 (three right-resizes from a 40|40 split), and a marker
551 # wall lands in the focused session. 551 # typed into the restored wall lands in the focused session.
552 LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$" 552 LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$"
553 defer_rm "$LPSTATE" 553 defer_rm "$LPSTATE"
554 LPHOSTS="$LPSTATE/mux/hosts" 554 LPHOSTS="$LPSTATE/mux/hosts"
@@ -567,14 +567,13 @@ await_out "$OUT.lprb" "lpr-origin-b" "layout-restore: session b marker never rea
567 pipe_detach 567 pipe_detach
568 wait_grid "$SOCK59" "lpr-origin-b" "layout-restore: session b marker" b 568 wait_grid "$SOCK59" "lpr-origin-b" "layout-restore: session b marker" b
569 569
570 # Write the hosts file by hand: ONE line, the daemon. Its two sessions are 570 # Write the hosts file by hand: ONE line, the daemon — and the layout that
571 # what the poll turns into two panes — the wall knows nothing about them 571 # names its two sessions, because the file is the wall and a pane is a leaf
572 # until it asks. 572 # somebody wrote down. What this leg then measures is the RESIZE: run 1
573 # moves the boundary the seed cut, and run 2 has to open on the moved one.
573 mkdir -p "$LPSTATE/mux" 574 mkdir -p "$LPSTATE/mux"
574 printf -- '--sock %s\n' "$SOCK59" > "$LPHOSTS" 575 printf -- '--sock %s\n' "$SOCK59" > "$LPHOSTS"
575 # ...and no saved tree yet: what this leg saves and restores has to be the 576 seed_layout "$LPSTATE" beside "--sock $SOCK59#0" "--sock $SOCK59#b"
576 # tree the FIRST ptyclient builds, not one an earlier terminal left here.
577 no_saved_tree "$LPSTATE"
578 577
579 set +e 578 set +e
580 # First ptyclient: hydrate, resize three right, detach. The detach saves 579 # First ptyclient: hydrate, resize three right, detach. The detach saves
@@ -676,20 +675,27 @@ D60PID=""
676 rm -rf "$LPSTATE" 675 rm -rf "$LPSTATE"
677 ok "a resized layout survives a detach/reattach round trip via the sidecar" 676 ok "a resized layout survives a detach/reattach round trip via the sidecar"
678 677
679 # ---- layout sidecar: healing on LIVE drift --------------------------- 678 # ---- a poll GRADES the panes and adds none ---------------------------
680 # 679 #
681 # The drift a wall of daemons can actually suffer, and it is no longer a 680 # The drift a wall of daemons can actually suffer, and it is no longer a
682 # file being edited: a 2-pane sidecar is saved, and then one of those 681 # file being edited: a 2-pane layout is saved, and then one of those
683 # sessions ENDS while another is born. On reattach, restoreLayout heals — 682 # sessions ENDS while another is born on the same daemon. What the next
684 # the survivor leaf keeps its session and the newcomer inserts beside it — 683 # poll may do about that is the whole claim, and it is now three things
685 # and the unmatched saved leaf STANDS, wearing `gone`. Three tiles, and the 684 # and no fourth:
686 # wall did not refuse.
687 # 685 #
688 # It used to be two: the unmatched leaf collapsed out on the first poll 686 # * the survivor keeps its pane, its session and its weight.
689 # answer. Gone panes are the change (2026-09-01) — a reachable host that 687 # * the pane whose session the list no longer names STANDS, wearing
690 # no longer names your saved session dresses that pane instead of taking 688 # `[session ended]` — the host answered and did not name it, which is
691 # it, so a reboot cannot silently re-cut a wall. The heal is the same 689 # an offer to start it again and not a rect to take away.
692 # code; only the fate of the leaf the list disowns is different. 690 # * the newcomer joins NOTHING. It is a live session on a listed daemon
691 # with no leaf naming it, so it is on no wall: not a tile, not a
692 # stripe, and not a line in the file the detach writes.
693 #
694 # The third is the one this leg exists for. Until 2026-09-02 a poll's list
695 # was where tiles came from, so a session born anywhere — by another
696 # terminal, by a script, by `mux a` — appeared on every wall over that
697 # daemon and re-cut it. The file is the wall now; the poll only grades what
698 # the file already names.
693 LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$" 699 LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$"
694 defer_rm "$LPHSTATE" 700 defer_rm "$LPHSTATE"
695 LPHHOSTS="$LPHSTATE/mux/hosts" 701 LPHHOSTS="$LPHSTATE/mux/hosts"
@@ -708,12 +714,11 @@ await_out "$OUT.lphb" "lph-doomed-b" "layout-heal: session b marker never reache
708 pipe_detach 714 pipe_detach
709 wait_grid "$SOCK60" "lph-doomed-b" "layout-heal: session b marker" b 715 wait_grid "$SOCK60" "lph-doomed-b" "layout-heal: session b marker" b
710 716
711 # One line, the daemon. Its two sessions are the two panes the first 717 # One line, the daemon, and a two-leaf layout over it: the wall run 1
712 # ptyclient saves a sidecar for. 718 # resizes and saves is authored here, not discovered by a poll.
713 mkdir -p "$LPHSTATE/mux" 719 mkdir -p "$LPHSTATE/mux"
714 printf -- '--sock %s\n' "$SOCK60" > "$LPHHOSTS" 720 printf -- '--sock %s\n' "$SOCK60" > "$LPHHOSTS"
715 # No saved tree yet, for the restore leg's reason. 721 seed_layout "$LPHSTATE" beside "--sock $SOCK60#0" "--sock $SOCK60#b"
716 no_saved_tree "$LPHSTATE"
717 722
718 set +e 723 set +e
719 XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 724 XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
@@ -742,10 +747,10 @@ rc0 "layout-heal: first ptyclient exited $RC:" "$OUT.lphpc1" "$OUT.lphcap1.err"
742 ls -la "$LPHSTATE/mux/" 2>&1; exit 1; } 747 ls -la "$LPHSTATE/mux/" 2>&1; exit 1; }
743 748
744 # Drift, and the daemon is the one that does it: session b ENDS and 749 # Drift, and the daemon is the one that does it: session b ENDS and
745 # session c is born. The sidecar still describes [0, b]; the wall the next 750 # session c is born. The layout still describes [0, b] and it is still the
746 # poll builds is [0, c]. On reattach, `0` is the survivor (matched), c 751 # wall — `0` binds, b's pane is graded gone, and c is a session on this
747 # inserts beside it, and b's saved leaf collapses out. Nothing was edited 752 # daemon that this wall has never heard of. Nothing was edited; this is the
748 # — this is the only kind of drift a list of daemons can suffer. 753 # only kind of drift a list of daemons can suffer.
749 pipe_mux "$OUT.lphb2" "$OUT.lphb2.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session b 754 pipe_mux "$OUT.lphb2" "$OUT.lphb2.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session b
750 pipe_send 'exit 0\n' 755 pipe_send 'exit 0\n'
751 pipe_waitexit "layout-heal: session b's shell" 756 pipe_waitexit "layout-heal: session b's shell"
@@ -761,7 +766,7 @@ XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
761 --out "$OUT.lphcap2" --err "$OUT.lphcap2.err" -- \ 766 --out "$OUT.lphcap2" --err "$OUT.lphcap2.err" -- \
762 "$MUX" > "$OUT.lphpc2" 2>&1 <<'EOF' 767 "$MUX" > "$OUT.lphpc2" 2>&1 <<'EOF'
763 expect lph-survivor-0 20000 768 expect lph-survivor-0 20000
764 settle 700 20000 769 settle 2500 30000
765 send \x1c1 770 send \x1c1
766 settle 500 15000 771 settle 500 15000
767 send printf 'lph-heal-%s\n' marker\n 772 send printf 'lph-heal-%s\n' marker\n
@@ -788,33 +793,46 @@ _lph_r1=$(rail_cols "$OUT.lphcap1" | awk '$1 > 1' | tail -1)
788 echo "e2e FAIL: layout-heal: run 1's rail ended at column ${_lph_r1:-none}, want >= 43" 793 echo "e2e FAIL: layout-heal: run 1's rail ended at column ${_lph_r1:-none}, want >= 43"
789 echo " — the resize this leg saves a tree for did not happen" 794 echo " — the resize this leg saves a tree for did not happen"
790 cat "$OUT.lphpc1"; exit 1; } 795 cat "$OUT.lphpc1"; exit 1; }
791 # The money assertion is that run 2's cut came from the SIDECAR and not 796 # The money assertion is that run 2's cut came from the FILE and not from
792 # from the aspect rule, and the tree run 2 SAVES is where that reads 797 # the aspect rule, and the tree run 2 SAVES is where that reads exactly. A
793 # exactly. A restore that reported failure falls through to 798 # seed that refused falls through to `setRootOrient(rootOrient(size))` and
794 # `setRootOrient(rootOrient(size))` and builds default weights of 1, so a 799 # builds default weights of 1, so a root that is still `beside` over a
795 # root that is still `beside` over a survivor still weighing 43 is the heal 800 # survivor still weighing 43 is the seed having run AND won. Measured
796 # having run AND won. Measured 2026-09-01, sidecar read either side: 801 # 2026-09-02, layout read either side:
797 # 802 #
798 # saved by run 1: beside 0 / leaf 43 #0 / leaf 36 #b / focus 0 803 # saved by run 1: beside 0 / leaf 43 #0 / leaf 36 #b / focus 0
799 # saved by run 2: beside 0 / leaf 43 #0 / leaf 43 #c / leaf 36 #b 804 # saved by run 2: beside 0 / leaf 43 #0 / leaf 36 #b
800 # 805 #
801 # Three leaves, in that order: `insert` puts the newcomer immediately after 806 # Two leaves, unchanged: the newcomer is not one of them, and the leaf the
802 # its anchor (the focus, `0`) and copies the anchor's weight, and the leaf 807 # list disowned keeps its own 36 where it stood. Nothing was added and
803 # the list disowned keeps its own 36 where it stood. Nothing collapsed, so 808 # nothing collapsed, so nothing re-wrapped — which is why the orientation
804 # nothing re-wrapped — which is why the orientation and the weights now 809 # and the weights survive a poll that used to rewrite both.
805 # survive a heal that used to lose both.
806 _lph_root=$(sed -n '2p' "$LPHLAYOUT") 810 _lph_root=$(sed -n '2p' "$LPHLAYOUT")
807 [ "$_lph_root" = "beside 0" ] || { 811 [ "$_lph_root" = "beside 0" ] || {
808 echo "e2e FAIL: layout-heal: run 2 saved root '$_lph_root', want 'beside 0'" 812 echo "e2e FAIL: layout-heal: run 2 saved root '$_lph_root', want 'beside 0'"
809 echo " — the aspect rule cut this wall, so the saved tree never reached it" 813 echo " — the aspect rule cut this wall, so the saved tree never reached it"
810 cat "$LPHLAYOUT"; exit 1; } 814 cat "$LPHLAYOUT"; exit 1; }
811 _lph_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$LPHLAYOUT" | tr '\n' ' ') 815 _lph_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$LPHLAYOUT" | tr '\n' ' ')
812 [ "$_lph_leaves" = "0 c b " ] || { 816 [ "$_lph_leaves" = "0 b " ] || {
813 echo "e2e FAIL: layout-heal: run 2 saved leaves '$_lph_leaves', want '0 c b ':" 817 echo "e2e FAIL: layout-heal: run 2 saved leaves '$_lph_leaves', want '0 b ':"
814 cat "$LPHLAYOUT"; exit 1; } 818 cat "$LPHLAYOUT"; exit 1; }
815 grep -q -- "^ leaf 43 .*#0\$" "$LPHLAYOUT" || { 819 grep -q -- "^ leaf 43 .*#0\$" "$LPHLAYOUT" || {
816 echo "e2e FAIL: layout-heal: the survivor's weight did not survive the heal:" 820 echo "e2e FAIL: layout-heal: the survivor's weight did not survive the poll:"
817 cat "$LPHLAYOUT"; exit 1; } 821 cat "$LPHLAYOUT"; exit 1; }
822 # The newcomer joined nothing. Two and a half seconds of quiet is several
823 # poll answers, and every one of them saw session c on this daemon's list;
824 # a wall that took its panes from that list would have cut a third stripe
825 # and painted c's own marker into it. `wc -l` on the matches and not
826 # `grep -c`, the hosts group's reason: a pty capture is escape bytes with
827 # almost no newlines in it.
828 [ "$(grep -aoF "$SOCK60#c" "$OUT.lphcap2" | wc -l)" -eq 0 ] || {
829 echo "e2e FAIL: layout-heal: a pane for session c is on the wall and no leaf"
830 echo " names it — the poll is birthing tiles again:"
831 cat "$OUT.lphpc2"; exit 1; }
832 [ "$(grep -aoF "lph-newcomer-c" "$OUT.lphcap2" | wc -l)" -eq 0 ] || {
833 echo "e2e FAIL: layout-heal: session c's own output painted on a wall whose"
834 echo " layout never named it:"
835 cat "$OUT.lphpc2"; exit 1; }
818 # ...and the leaf the list disowned says on screen why it is still there. 836 # ...and the leaf the list disowned says on screen why it is still there.
819 # The state word survives a bar's truncation where the socket path does 837 # The state word survives a bar's truncation where the socket path does
820 # not, so this is the needle that reads at any pane width. 838 # not, so this is the needle that reads at any pane width.
@@ -828,15 +846,27 @@ grep -q "lph-heal-marker" "$OUT.lphfa" || {
828 assert_stopped "$SOCK60" "$D61PID" "layout-heal" "$OUT.lphstop" 846 assert_stopped "$SOCK60" "$D61PID" "layout-heal" "$OUT.lphstop"
829 D61PID="" 847 D61PID=""
830 rm -rf "$LPHSTATE" 848 rm -rf "$LPHSTATE"
831 ok "the layout heals on live drift: the survivor keeps its session, the newcomer joins, the ended one's pane stands gone" 849 ok "a poll changes nothing on the wall: a newcomer stays off it, the survivor keeps its pane, the ended one's pane stands gone"
832 850
833 # ---- layout sidecar: silent degrade on garbage ----------------------- 851 # ---- layout sidecar: silent degrade on garbage -----------------------
834 # 852 #
835 # A corrupted sidecar (`bogus 9`) is not a fatal error: restoreLayout 853 # A corrupted sidecar (`bogus 9`) is not a fatal error: restoreLayout
836 # returns null, the default tree is built, and the wall comes up. The 854 # returns null, the default tree is built, and the wall comes up. The
837 # assertion is that a marker typed into the wall reaches the focused 855 # assertion is that a marker typed into the wall reaches the focused
838 # session (the wall works), and the client's stderr never mentions 856 # session (the wall works), and that the refusal NAMES the line it gave up
839 # "layout" (the degrade is silent). 857 # on so there is something to fix.
858 #
859 # Loud and not silent, which is the change: the layout used to be derived
860 # convenience — a stale cut was a convenience to forget — and it is the
861 # WALL now. A file the user authored and mux threw away without a word is
862 # panes gone with no undo and no sentence to search for, so the refusal is
863 # printed once, before the alternate screen, with the offending line in it.
864 #
865 # `mux --sock S` and not a bare `mux`, because a refused file leaves the
866 # wall with no panes at all: the entry attach is what gives this leg a
867 # stripe to type into, and session b — live on the same daemon, named by
868 # nothing — is what makes "the entry pane ALONE" a claim rather than a
869 # count of one.
840 LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$" 870 LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$"
841 defer_rm "$LPDSTATE" 871 defer_rm "$LPDSTATE"
842 LPDHOSTS="$LPDSTATE/mux/hosts" 872 LPDHOSTS="$LPDSTATE/mux/hosts"
@@ -850,18 +880,25 @@ await_out "$OUT.lpda" "lpd-origin" "layout-degrade: the default session's marker
850 pipe_detach 880 pipe_detach
851 wait_grid "$SOCK61" "lpd-origin" "layout-degrade: the default session's marker" 881 wait_grid "$SOCK61" "lpd-origin" "layout-degrade: the default session's marker"
852 882
883 pipe_mux "$OUT.lpdb" "$OUT.lpdb.err" env XDG_STATE_HOME="$LPDSTATE" timeout 40 "$MUX" --sock "$SOCK61" --session b
884 pipe_send 'printf "lpd-%%s\\n" bee\n'
885 await_out "$OUT.lpdb" "lpd-bee" "layout-degrade: session b's marker never reached the client"
886 pipe_detach
887 wait_grid "$SOCK61" "lpd-bee" "layout-degrade: session b's marker" b
888
853 mkdir -p "$LPDSTATE/mux" 889 mkdir -p "$LPDSTATE/mux"
854 printf -- '--sock %s\n' "$SOCK61" > "$LPDHOSTS" 890 printf -- '--sock %s\n' "$SOCK61" > "$LPDHOSTS"
855 # A garbage sidecar: wrong magic, wrong version. restoreLayout returns 891 # A garbage layout: wrong magic, wrong version. `parseReporting` gives up
856 # null on the header check and the caller builds the default tree. 892 # on the first line and hands it back, `seedSidecar` prints it and returns
893 # null, and the wall starts as if the file were not there.
857 printf 'bogus 9\n' > "$LPDLAYOUT" 894 printf 'bogus 9\n' > "$LPDLAYOUT"
858 895
859 set +e 896 set +e
860 XDG_STATE_HOME="$LPDSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ 897 XDG_STATE_HOME="$LPDSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
861 --out "$OUT.lpdcap" --err "$OUT.lpdcap.err" -- \ 898 --out "$OUT.lpdcap" --err "$OUT.lpdcap.err" -- \
862 "$MUX" > "$OUT.lpdpc" 2>&1 <<'EOF' 899 "$MUX" --sock "$SOCK61" > "$OUT.lpdpc" 2>&1 <<'EOF'
863 expect lpd-origin 20000 900 expect lpd-origin 20000
864 settle 700 20000 901 settle 2000 25000
865 send printf 'lpd-degrade-%s\n' marker\n 902 send printf 'lpd-degrade-%s\n' marker\n
866 expect lpd-degrade-marker 10000 903 expect lpd-degrade-marker 10000
867 settle 500 15000 904 settle 500 15000
@@ -874,16 +911,46 @@ rc0 "layout-degrade: ptyclient exited $RC:" "$OUT.lpdpc" "$OUT.lpdcap.err"
874 # The wall came up: the marker reached the focused session. 911 # The wall came up: the marker reached the focused session.
875 timeout 20 "$MUX" a capture --sock "$SOCK61" > "$OUT.lpdfa" 2>&1 912 timeout 20 "$MUX" a capture --sock "$SOCK61" > "$OUT.lpdfa" 2>&1
876 grep -q "lpd-degrade-marker" "$OUT.lpdfa" || { 913 grep -q "lpd-degrade-marker" "$OUT.lpdfa" || {
877 echo "e2e FAIL: layout-degrade: marker not in session a (wall did not come up):" 914 echo "e2e FAIL: layout-degrade: marker not in session 0 (wall did not come up):"
878 cat "$OUT.lpdfa"; exit 1; } 915 cat "$OUT.lpdfa"; exit 1; }
879 # The degrade was silent: no mention of "layout" on the client's stderr. 916 # The refusal was SAID, and it named the file and the line. Read off the
880 grep -qi "layout" "$OUT.lpdcap.err" && { 917 # client's own stderr file, which is where a sentence printed before the
881 echo "e2e FAIL: layout-degrade: stderr mentions layout (degrade was not silent):" 918 # alternate screen lands — on a real terminal that is the shell prompt the
919 # user typed `mux` at.
920 grep -qF "mux: layout ignored (" "$OUT.lpdcap.err" || {
921 echo "e2e FAIL: layout-degrade: a refused layout said nothing; stderr holds:"
922 cat "$OUT.lpdcap.err"; exit 1; }
923 grep -qF "bogus 9" "$OUT.lpdcap.err" || {
924 echo "e2e FAIL: layout-degrade: the refusal never printed the line it gave up on:"
882 cat "$OUT.lpdcap.err"; exit 1; } 925 cat "$OUT.lpdcap.err"; exit 1; }
926 # The entry pane ALONE. Session b is live on this daemon and no leaf names
927 # it, so a wall of one is the whole screen — no second stripe, and no rail
928 # to cut it with. `rail_cols` reads a bar at column 1 as a rail, so the
929 # filter is the gone-panes leg's: a column of its own past the first.
930 [ "$(grep -aoF "lpd-bee" "$OUT.lpdcap" | wc -l)" -eq 0 ] || {
931 echo "e2e FAIL: layout-degrade: session b painted on a wall that refused its"
932 echo " file and named nothing else:"
933 cat "$OUT.lpdpc"; exit 1; }
934 _lpd_rail=$(rail_cols "$OUT.lpdcap" | awk '$1 > 1' | tail -1)
935 [ -z "$_lpd_rail" ] || {
936 echo "e2e FAIL: layout-degrade: a rail at column $_lpd_rail — the refused wall"
937 echo " came up with more than the entry pane:"
938 cat "$OUT.lpdpc"; exit 1; }
939 # ...and what the detach wrote is a wall of one: the file the user has to
940 # fix is replaced by the wall they actually got, not left as the garbage
941 # that was refused.
942 _lpd_saved=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$LPDLAYOUT" | tr '\n' ' ')
943 [ "$_lpd_saved" = "--sock $SOCK61#0 " ] || {
944 echo "e2e FAIL: layout-degrade: the detach saved leaves '$_lpd_saved',"
945 echo " want the entry pane alone; the file holds:"
946 cat "$LPDLAYOUT"; exit 1; }
947 head -1 "$LPDLAYOUT" | grep -qxF 'mux-layout 1' || {
948 echo "e2e FAIL: layout-degrade: the file the detach wrote is not a layout:"
949 cat "$LPDLAYOUT"; exit 1; }
883 assert_stopped "$SOCK61" "$D62PID" "layout-degrade" "$OUT.lpdstop" 950 assert_stopped "$SOCK61" "$D62PID" "layout-degrade" "$OUT.lpdstop"
884 D62PID="" 951 D62PID=""
885 rm -rf "$LPDSTATE" 952 rm -rf "$LPDSTATE"
886 ok "a corrupted sidecar degrades silently to the default layout" 953 ok "a corrupted layout is reported with its line and the wall starts as if it were missing"
887 954
888 # ---- gone panes: a rebooted daemon keeps the cut ---------------------- 955 # ---- gone panes: a rebooted daemon keeps the cut ----------------------
889 # 956 #
@@ -920,13 +987,12 @@ await_out "$OUT.gpc" "gp-sea" "gone-panes: session c's marker never reached the
920 pipe_detach 987 pipe_detach
921 wait_grid "$SOCK62" "gp-sea" "gone-panes: session c's marker" c 988 wait_grid "$SOCK62" "gp-sea" "gone-panes: session c's marker" c
922 989
923 # One line, the daemon. Its three sessions are what the poll turns into 990 # One line, the daemon, and the three-leaf layout that IS this wall. Run 1
924 # three panes; the wall knows nothing about them until it asks. 991 # does not discover these panes; it paints the file and saves it back, and
992 # the cut it saves is what every later run is measured against.
925 mkdir -p "$GPSTATE/mux" 993 mkdir -p "$GPSTATE/mux"
926 printf -- '--sock %s\n' "$SOCK62" > "$GPHOSTS" 994 printf -- '--sock %s\n' "$SOCK62" > "$GPHOSTS"
927 # No saved tree yet, for the restore leg's reason: what this leg saves and 995 seed_layout "$GPSTATE" beside "--sock $SOCK62#0" "--sock $SOCK62#b" "--sock $SOCK62#c"
928 # reads back has to be the tree run 1 builds.
929 no_saved_tree "$GPSTATE"
930 996
931 set +e 997 set +e
932 # Run 1: hydrate the three-pane wall on a tty and detach. The detach saves 998 # Run 1: hydrate the three-pane wall on a tty and detach. The detach saves
@@ -946,9 +1012,9 @@ rc0 "gone-panes: run 1's ptyclient exited $RC:" "$OUT.gppc1" "$OUT.gpcap1.err"
946 echo "e2e FAIL: gone-panes: sidecar not saved at $GPLAYOUT" 1012 echo "e2e FAIL: gone-panes: sidecar not saved at $GPLAYOUT"
947 ls -la "$GPSTATE/mux/" 2>&1; exit 1; } 1013 ls -la "$GPSTATE/mux/" 2>&1; exit 1; }
948 # Three leaves, and the ORDER matters below: a seeded pane takes the tile 1014 # Three leaves, and the ORDER matters below: a seeded pane takes the tile
949 # index of its leaf, so `Ctrl-\ 2` is leaf 1 and `Ctrl-\ 3` is leaf 2. Pin 1015 # index of its leaf, so `Ctrl-\ 2` is leaf 1 and `Ctrl-\ 3` is leaf 2. Read
950 # the mapping here rather than in a comment, or a change in the order the 1016 # back off the file run 1 SAVED rather than assumed from the file it was
951 # poll births tiles would silently retarget every chord this leg sends. 1017 # seeded with, so a save that reordered the tree retargets nothing silently.
952 _gp_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ') 1018 _gp_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ')
953 [ "$_gp_leaves" = "0 b c " ] || { 1019 [ "$_gp_leaves" = "0 b c " ] || {
954 echo "e2e FAIL: gone-panes: sidecar leaves are '$_gp_leaves', want '0 b c ':" 1020 echo "e2e FAIL: gone-panes: sidecar leaves are '$_gp_leaves', want '0 b c ':"
test/e2e_13_birth.sh
Old New
@@ -32,24 +32,31 @@ defer_rm "$LFSTATE"
32 # ---- the picker's `a` adds a HOST by spelling -------------------------- 32 # ---- the picker's `a` adds a HOST by spelling --------------------------
33 # 33 #
34 # `mux hosts add` typed from inside. A spelling names a DAEMON: the line 34 # `mux hosts add` typed from inside. A spelling names a DAEMON: the line
35 # goes into the hosts file, a poller starts, and that daemon's sessions 35 # goes into the hosts file and a poller starts — and that is ALL it does.
36 # become tiles — the editor cannot conjure a session, only ask a machine 36 # The daemon's sessions become ROWS, not panes: the wall is the layout
37 # what it is running. A bad spelling is a notice and nothing else (the 37 # file, so nothing reaches it that the user did not put there. Enter on the
38 # file does not grow). Esc eats the line: the shell never sees it, and 38 # host opens its sessions and Enter on a session is what adds one. A bad
39 # after the popup closes the next keys reach the session again. 39 # spelling is a notice and nothing else (the file does not grow). Esc eats
40 # the line: the shell never sees it, and after the popup closes the next
41 # keys reach the session again.
40 # 42 #
41 # The editor lives INSIDE the picker (`Ctrl-\ s`, then `a`), so its Esc 43 # The editor lives INSIDE the picker (`Ctrl-\ s`, then `a`), so its Esc
42 # goes back to the rows and a second Esc is what closes the popup. That is 44 # goes back to the rows and a second Esc is what closes the popup. That is
43 # also why the whole spelling round happens under ONE `\x1c s`: an `s` 45 # also why the whole spelling round happens under ONE `\x1c s`: an `s`
44 # typed at an open picker closes it. 46 # typed at an open picker closes it.
45 # 47 #
46 # The prompt echoes what is typed, so the added host's witness is not its 48 # Two runs, and the split is the claim. The prompt echoes what is typed, so
47 # label on the capture (the echo would match) but daemon B's OWN SHELL's 49 # neither the added host's label on the capture nor its row proves
48 # output painting on this wall — a marker B printed before this client 50 # anything about panes; what does is daemon B's OWN SHELL's output — a
49 # existed, which only a real attach to B could put here. 51 # marker B printed before either client existed, which only a real attach
52 # to B can paint. Run 1 adds the host and must NOT hold that marker; run 2
53 # walks the picker down to the session and must. One run could not say it:
54 # ptyclient's expect cursor only moves forward, so a capture holding the
55 # marker cannot be asked whether it held it before the Enter.
50 PRSTATE="${TMPDIR:-/tmp}/mux-e2e-prompt-state-$$" 56 PRSTATE="${TMPDIR:-/tmp}/mux-e2e-prompt-state-$$"
51 defer_rm "$PRSTATE" 57 defer_rm "$PRSTATE"
52 PRHOSTS="$PRSTATE/mux/hosts" 58 PRHOSTS="$PRSTATE/mux/hosts"
59 PRLAYOUT="$PRSTATE/mux/layout"
53 # Two daemons, because the assertion is that the birth landed on the one 60 # Two daemons, because the assertion is that the birth landed on the one
54 # the spelling NAMED: one socket could only ever say a session was created, 61 # the spelling NAMED: one socket could only ever say a session was created,
55 # not which daemon created it. 62 # not which daemon created it.
@@ -78,7 +85,10 @@ wait_grid "$SOCK63" "pr-bmark" "prompt: daemon B's marker"
78 85
79 mkdir -p "$PRSTATE/mux" 86 mkdir -p "$PRSTATE/mux"
80 printf -- '--sock %s\n' "$SOCK62" > "$PRHOSTS" 87 printf -- '--sock %s\n' "$SOCK62" > "$PRHOSTS"
81 no_saved_tree "$PRSTATE" 88 # One line and one leaf: the wall this run starts on is daemon A's session
89 # 0 and nothing else, so every pane that appears after it is one the
90 # picker put there.
91 seed_layout "$PRSTATE" beside "--sock $SOCK62#0"
82 92
83 # The spelling goes in TWO sends with a settle between: the prompt paints 93 # The spelling goes in TWO sends with a settle between: the prompt paints
84 # its echo when a read ENDS with the prompt still open, so a spelling that 94 # its echo when a read ENDS with the prompt still open, so a spelling that
@@ -103,8 +113,10 @@ settle 400 15000
103 send azzz\x1b 113 send azzz\x1b
104 settle 400 15000 114 settle 400 15000
105 send \x1b 115 send \x1b
106 expect pr-bmark 20000 116 # Several poll answers with daemon B listed and no leaf naming any of its
107 settle 700 20000 117 # sessions. A wall that took its panes from a host's list would cut a
118 # second stripe in here, and B's marker would be on the screen below.
119 settle 2500 25000
108 send printf 'pr-after-%s\n' esc\n 120 send printf 'pr-after-%s\n' esc\n
109 expect pr-after-esc 10000 121 expect pr-after-esc 10000
110 settle 500 15000 122 settle 500 15000
@@ -139,15 +151,15 @@ grep -q -- "-A nosuchhost.invalid \[" "$OUT.prgrid" && {
139 grep -aq ": --sock " "$OUT.prcap" || { 151 grep -aq ": --sock " "$OUT.prcap" || {
140 echo "e2e FAIL: prompt: the prompt line never painted its echo:" 152 echo "e2e FAIL: prompt: the prompt line never painted its echo:"
141 cat "$OUT.prpc"; exit 1; } 153 cat "$OUT.prpc"; exit 1; }
142 # Daemon B is really on this wall: its session got a tile of its own, 154 # Daemon B is on the WALL FILE and not on the wall: adding a host starts a
143 # naming B's socket, and the focus never left daemon A — the keys typed 155 # poller and nothing else. B's own shell's output is the witness and it
144 # after the Esc are in A's session and in nothing of B's. 156 # must be absent — the marker cannot be an echo, B printed it before this
145 # B's own shell's output, on THIS wall's terminal. Not the bar: two 157 # process existed, so a hit here is a pane. `wc -l` on the matches rather
146 # tiles over 100 columns cut a socket path in half, and `labelText` 158 # than `grep -c`, the hosts group's reason: a pty capture is escape bytes
147 # spends what is left on the state word. The marker cannot be an echo — 159 # with almost no newlines in it.
148 # B printed it before this process existed. 160 [ "$(grep -aoF "pr-bmark" "$OUT.prcap" | wc -l)" -eq 0 ] || {
149 grep -qa "pr-bmark" "$OUT.prcap" || { 161 echo "e2e FAIL: prompt: adding a host put its session on the wall — the"
150 echo "e2e FAIL: prompt: the added host's session never painted on the wall:" 162 echo " picker's a is a hosts-file edit, not a pane:"
151 cat "$OUT.prpc"; exit 1; } 163 cat "$OUT.prpc"; exit 1; }
152 timeout 20 "$MUX" a capture --sock "$SOCK62" > "$OUT.prfa" 2>&1 164 timeout 20 "$MUX" a capture --sock "$SOCK62" > "$OUT.prfa" 2>&1
153 grep -q "pr-after-esc" "$OUT.prfa" || { 165 grep -q "pr-after-esc" "$OUT.prfa" || {
@@ -169,12 +181,49 @@ grep -qxF -- "--sock $SOCK63" "$PRHOSTS" || {
169 [ "$(grep -c . "$PRHOSTS")" = "2" ] || { 181 [ "$(grep -c . "$PRHOSTS")" = "2" ] || {
170 echo "e2e FAIL: prompt: hosts file is not exactly two lines (a refused spelling recorded?):" 182 echo "e2e FAIL: prompt: hosts file is not exactly two lines (a refused spelling recorded?):"
171 cat "$PRHOSTS"; exit 1; } 183 cat "$PRHOSTS"; exit 1; }
184
185 # Run 2: the two Enters that DO put B's session on this wall. The wall run
186 # 1 detached from is the file it saved — one leaf on daemon A — so the
187 # picker is where the second pane has to come from. `2` selects the row
188 # the `a` above added, the first Enter opens that host's sessions, and the
189 # second adds the row under the cursor. B's marker is the witness again,
190 # and now it must be there.
191 set +e
192 XDG_STATE_HOME="$PRSTATE" timeout 90 "$PTYCLIENT" --cols 100 --rows 30 \
193 --out "$OUT.pr2cap" --err "$OUT.pr2cap.err" -- \
194 "$MUX" > "$OUT.pr2pc" 2>&1 <<EOF
195 expect pr-origin 20000
196 settle 700 20000
197 send \x1cs
198 expect x forget 15000
199 send 2
200 settle 400 15000
201 send \r
202 expect Enter add to wall 15000
203 send \r
204 expect pr-bmark 25000
205 settle 1000 20000
206 send \x1cd
207 waitexit 15000
208 EOF
209 RC=$?
210 set -e
211 rc0 "prompt: the add-a-session leg exited $RC (did Enter on a session row make a pane?):" \
212 "$OUT.pr2pc" "$OUT.pr2cap.err"
213 # The pane is in the FILE, which is the wall: a pane that painted and was
214 # never written down is one the next `mux` loses.
215 grep -q -- "leaf [0-9][0-9]* --sock $SOCK63#0\$" "$PRLAYOUT" || {
216 echo "e2e FAIL: prompt: the added session is not a leaf of the saved wall:"
217 cat "$PRLAYOUT"; exit 1; }
218 grep -q -- "leaf [0-9][0-9]* --sock $SOCK62#0\$" "$PRLAYOUT" || {
219 echo "e2e FAIL: prompt: adding a pane lost the one that was already there:"
220 cat "$PRLAYOUT"; exit 1; }
172 assert_stopped "$SOCK62" "$D63PID" "prompt A" "$OUT.prastop" 221 assert_stopped "$SOCK62" "$D63PID" "prompt A" "$OUT.prastop"
173 D63PID="" 222 D63PID=""
174 assert_stopped "$SOCK63" "$D64PID" "prompt B" "$OUT.prbstop" 223 assert_stopped "$SOCK63" "$D64PID" "prompt B" "$OUT.prbstop"
175 D64PID="" 224 D64PID=""
176 rm -rf "$PRSTATE" 225 rm -rf "$PRSTATE"
177 ok "the picker's a adds a host by spelling: its sessions become tiles, recorded, refusals narrated, Esc eats the line" 226 ok "the picker's a adds a host by spelling: its sessions are listed in the picker, not put on the wall; Enter on one adds it"
178 227
179 228
180 # --------------------------------------------------------------------------- 229 # ---------------------------------------------------------------------------
@@ -294,11 +343,14 @@ SPSHIM
294 chmod +x "$SPDIR/ssh" 343 chmod +x "$SPDIR/ssh"
295 344
296 # One HOST line — a daemon, never a session, which is the whole of what a 345 # One HOST line — a daemon, never a session, which is the whole of what a
297 # host line may say. Its tile is that daemon's own session `0`; the attach 346 # host line may say — and one LEAF naming that daemon's own session `0`,
298 # the stand-in then sends names `spinghost`, which the daemon does not 347 # because the hub serves the layout's panes and a hosts file alone is a
299 # have, and the hub forwards the refusal untouched. 348 # wall with nothing on it. The attach the stand-in then sends names
349 # `spinghost`, which the daemon does not have, and the hub forwards the
350 # refusal untouched.
300 mkdir -p "$SPSTATE/mux" 351 mkdir -p "$SPSTATE/mux"
301 printf 'mux-spin@127.0.0.1\n' > "$SPSTATE/mux/hosts" 352 printf 'mux-spin@127.0.0.1\n' > "$SPSTATE/mux/hosts"
353 seed_layout "$SPSTATE" stacked "mux-spin@127.0.0.1#0"
302 354
303 XDG_STATE_HOME="$SPSTATE" XDG_CACHE_HOME="$SPSTATE/cache" PATH="$SPDIR:$PATH" \ 355 XDG_STATE_HOME="$SPSTATE" XDG_CACHE_HOME="$SPSTATE/cache" PATH="$SPDIR:$PATH" \
304 "$MUX" web --port "$SPPORT" > "$OUT.sph" 2>&1 & 356 "$MUX" web --port "$SPPORT" > "$OUT.sph" 2>&1 &
@@ -361,36 +413,40 @@ ok "a refusal the birth cannot fix backs off instead of spinning"
361 413
362 # ---- a new tile takes the lowest free digit ----------------------------- 414 # ---- a new tile takes the lowest free digit -----------------------------
363 # 415 #
364 # Create 1 2 3, end 2, create again: the wall says 2, not 4. The digits are 416 # Create 1 2 3, end the session under 2 and take its pane off the wall,
365 # what `Ctrl-\ 1-9` addresses, so a digit an ended session kept forever 417 # create again: the wall says 2, not 4. The digits are what `Ctrl-\ 1-9`
366 # would push every later birth out of the keyboard's reach — and the daemon 418 # addresses, so a digit a removed pane kept forever would push every later
367 # has already taken the NAME back (`client.nextFreeName`), so a wall that 419 # birth out of the keyboard's reach — and the daemon has taken the NAME
368 # appended would be showing tile 4 holding session 1. 420 # back (`client.nextFreeName`), so a wall that appended would be showing
421 # tile 4 holding session 1.
369 # 422 #
370 # Reuse, not renumbering: the tiles that stayed keep the digits their user 423 # Reuse, not renumbering: the tiles that stayed keep the digits their user
371 # learned, which is why this leg asserts 2 came back rather than that the 424 # learned, which is why this leg asserts 2 came back rather than that the
372 # wall reads 1 2 3 from the left. 425 # wall reads 1 2 3 from the left.
373 # 426 #
427 # The SHELL is what ends it now, not a chord: `Ctrl-\ x` takes a pane off
428 # the wall and leaves the session running, so it can free a digit but
429 # never a name. `exit` typed into the focused pane frees both — the
430 # daemon drops the session and the tile's own pump reads the exit_status
431 # and vanishes the pane (`endAction`'s `.exited` branch, which keeps the
432 # wall up because there are other tiles on it). One key, two holes.
433 #
374 # ONE ptyclient run, because the claim is about a LIVE wall taking a hole 434 # ONE ptyclient run, because the claim is about a LIVE wall taking a hole
375 # back: a second client would build its tiles from the poll and land on 435 # back: a second client would seed its panes from the file this one saved
376 # 1 2 by counting, testing nothing at all. 436 # and land on 1 2 by reading, testing nothing at all.
377 # 437 #
378 # The convergence points are relayout's screen clear — `\x1cc` re-cuts the 438 # The convergence points are relayout's screen clear — `\x1cc` re-cuts the
379 # wall. `\x1c2` emits none of its own (`setFocus` repaints bars, it does not 439 # wall, and so does the vanish the shell's exit causes. `\x1c2` emits none
380 # clear), which is what makes the clear after the `x` the ended tile's and 440 # of its own (`setFocus` repaints bars, it does not clear), which is what
381 # not the focus move's. 441 # makes the clear after the exit the vanished pane's and not the focus
442 # move's.
382 # 443 #
383 # The four seconds after that clear are a DURATION and not a convergence 444 # The settle after the exit is a DURATION and not a convergence point: what
384 # point, because there is nothing left to converge on: measured on this 445 # it waits out is the POLL that takes the freed name out of the host's
385 # harness, `x` yields exactly ONE clear, and a second `expect \x1b[2J` in 446 # list, and a `c` pressed before it would find the daemon still listing the
386 # place of this settle times out. The tile's departure is not separately 447 # name it has freed and birth session 3. That reads here as the session-1
387 # announced, so the wait is the daemon's LIST — `x` leaves a dead tile 448 # assertions below failing, never as a green run, which is what makes a
388 # narrating its exit and the tile goes on the next poll a second later. 449 # duration acceptable.
389 #
390 # A `c` pressed before that finds the dying tile still wearing the name the
391 # daemon has already freed and merely focuses it, which reads here as
392 # `lfd-back` never arriving and as the three assertions below failing —
393 # never as a green run. That is what makes a duration acceptable here.
394 # 450 #
395 # The clear after the LAST `\x1cc` is the other half of the claim: a birth 451 # The clear after the LAST `\x1cc` is the other half of the claim: a birth
396 # that takes a digit back leaves the slot high-water mark where it was, so 452 # that takes a digit back leaves the slot high-water mark where it was, so
@@ -422,9 +478,9 @@ expect lfd-three 15000
422 settle 400 15000 478 settle 400 15000
423 send \x1c2 479 send \x1c2
424 settle 700 15000 480 settle 700 15000
425 send \x1cx 481 send exit\n
426 expect \x1b[2J 25000 482 expect \x1b[2J 25000
427 settle 4000 25000 483 settle 3000 25000
428 send \x1cc 484 send \x1cc
429 expect \x1b[2J 20000 485 expect \x1b[2J 20000
430 settle 500 15000 486 settle 500 15000
@@ -437,8 +493,8 @@ EOF
437 RC=$? 493 RC=$?
438 set -e 494 set -e
439 [ "$RC" -eq 0 ] || { 495 [ "$RC" -eq 0 ] || {
440 echo "e2e FAIL: lowest-free-digit: ptyclient leg exited $RC (did the x chord" 496 echo "e2e FAIL: lowest-free-digit: ptyclient leg exited $RC (did the middle"
441 echo " end the middle session, and did the birth after it land?):" 497 echo " pane's shell exit take its pane, and did the birth land?):"
442 cat "$OUT.lfdpc"; exit 1; } 498 cat "$OUT.lfdpc"; exit 1; }
443 # The oracle, not the byte stream: every digit this leg typed its way 499 # The oracle, not the byte stream: every digit this leg typed its way
444 # through is somewhere in the capture, so only the final grid says which 500 # through is somewhere in the capture, so only the final grid says which
@@ -480,10 +536,10 @@ ok "a new tile takes the lowest free digit, and the daemon takes the name back"
480 # 536 #
481 # The refusal is the client TABLE, not the session table, and that is what 537 # The refusal is the client TABLE, not the session table, and that is what
482 # makes this cheap: every tile is its own attach and so its own client 538 # makes this cheap: every tile is its own attach and so its own client
483 # slot, so a daemon holding eight sessions on a terminal with room for ten 539 # slot, so a wall of eight panes on a terminal with room for ten stripes
484 # stripes has eight tiles and no slot left. The picker's Enter dials a 540 # holds every slot the daemon has. The picker's `c` births a ninth and
485 # ninth, the daemon refuses it before any replay frame, and the pump paints 541 # dials it, the daemon refuses that attach before any replay frame, and the
486 # `[refused]` on that tile's bar on its way out. 542 # pump paints `[refused]` on that tile's bar on its way out.
487 # 543 #
488 # Two claims, and the second is the newer one: the wall STAYS. A refused 544 # Two claims, and the second is the newer one: the wall STAYS. A refused
489 # picker birth used to take mux down with it (rc 1 on a terminal); a tile 545 # picker birth used to take mux down with it (rc 1 on a terminal); a tile
@@ -503,7 +559,12 @@ fill_sessions "$SOCK75" "$OUT.rtfill" f 1 7
503 wait_sessions "$SOCK75" 8 "refused-tile: the daemon should hold eight sessions" 559 wait_sessions "$SOCK75" 8 "refused-tile: the daemon should hold eight sessions"
504 mkdir -p "$RTSTATE/mux" 560 mkdir -p "$RTSTATE/mux"
505 printf -- '--sock %s\n' "$SOCK75" > "$RTSTATE/mux/hosts" 561 printf -- '--sock %s\n' "$SOCK75" > "$RTSTATE/mux/hosts"
506 no_saved_tree "$RTSTATE" 562 # Eight leaves, one per session: the wall is the file, so a saturated
563 # client table is eight panes somebody wrote down and not eight the poll
564 # went and found.
565 seed_layout "$RTSTATE" stacked \
566 "--sock $SOCK75#0" "--sock $SOCK75#f1" "--sock $SOCK75#f2" "--sock $SOCK75#f3" \
567 "--sock $SOCK75#f4" "--sock $SOCK75#f5" "--sock $SOCK75#f6" "--sock $SOCK75#f7"
507 set +e 568 set +e
508 # Tall and not wide, the hosts group's shape and for its reason: 80 columns 569 # Tall and not wide, the hosts group's shape and for its reason: 80 columns
509 # is under the aspect rule's `cols >= 2 * rows`, so the cut is stacked and 570 # is under the aspect rule's `cols >= 2 * rows`, so the cut is stacked and
@@ -518,8 +579,8 @@ XDG_STATE_HOME="$RTSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \
518 expect [up] 25000 579 expect [up] 25000
519 settle 2000 25000 580 settle 2000 25000
520 send \x1cs 581 send \x1cs
521 expect Enter/c new session 15000 582 expect x forget 15000
522 send \r 583 send c
523 expect [refused] 25000 584 expect [refused] 25000
524 settle 1500 20000 585 settle 1500 20000
525 send printf 'rt-%s\n' alive\n 586 send printf 'rt-%s\n' alive\n
test/e2e_15_askpass.sh
Old New
@@ -140,11 +140,16 @@ ASKAPID=$DPID
140 # fake box across the shim. No tile is EVER born off the box: it refuses 140 # fake box across the shim. No tile is EVER born off the box: it refuses
141 # every BatchMode run, so its poll never returns a list and its picker row 141 # every BatchMode run, so its poll never returns a list and its picker row
142 # reads `unreachable`. The prompt is raised by the one dial a user asks 142 # reads `unreachable`. The prompt is raised by the one dial a user asks
143 # for — the picker's Enter on that row — which is the real path. 143 # for — the picker's `c` on that row — which is the real path.
144 askpass_wall() { 144 askpass_wall() {
145 mkdir -p "$1/mux" 145 mkdir -p "$1/mux"
146 printf -- '--sock %s\n%s\n' "$ASKSOCK_A" "$2" > "$1/mux/hosts" 146 printf -- '--sock %s\n%s\n' "$ASKSOCK_A" "$2" > "$1/mux/hosts"
147 no_saved_tree "$1" 147 # One pane, the LOCAL daemon's own session: the wall is the layout, so
148 # without a leaf these legs would open on the empty-wall line and the
149 # marker they wait for before touching the popup would never paint.
150 # Nothing on the box is ever a pane — it refuses every BatchMode run —
151 # which is what leaves the picker as the only road to a dial.
152 seed_layout "$1" stacked "--sock $ASKSOCK_A#0"
148 } 153 }
149 154
150 # ---- a password prompt is painted, masked, and answered ---------------- 155 # ---- a password prompt is painted, masked, and answered ----------------
@@ -189,7 +194,7 @@ send \x1cs
189 expect unreachable: Permission denied 15000 194 expect unreachable: Permission denied 15000
190 send 2 195 send 2
191 settle 400 15000 196 settle 400 15000
192 send \r 197 send c
193 expect askbox's password: 25000 198 expect askbox's password: 25000
194 send hunt 199 send hunt
195 settle 400 15000 200 settle 400 15000
@@ -295,7 +300,7 @@ send \x1cs
295 expect unreachable: Host key verification 15000 300 expect unreachable: Host key verification 15000
296 send 2 301 send 2
297 settle 400 15000 302 settle 400 15000
298 send \r 303 send c
299 expect continue connecting 25000 304 expect continue connecting 25000
300 send yes 305 send yes
301 settle 400 15000 306 settle 400 15000
@@ -351,7 +356,7 @@ send \x1cs
351 expect unreachable: Permission denied 15000 356 expect unreachable: Permission denied 15000
352 send 2 357 send 2
353 settle 400 15000 358 settle 400 15000
354 send \r 359 send c
355 expect askbox's password: 25000 360 expect askbox's password: 25000
356 send \x1b 361 send \x1b
357 expect prompt declined 25000 362 expect prompt declined 25000
@@ -448,6 +453,11 @@ ok "the entry dial is handed no prompt socket and keeps ssh's own"
448 # 453 #
449 # A comment cannot fail. Without this leg a future fallback to /tmp would 454 # A comment cannot fail. Without this leg a future fallback to /tmp would
450 # pass every other check in this file green. 455 # pass every other check in this file green.
456
457 # The wall this leg opens is written fresh: the password leg above left a
458 # layout with the pane its birth made, and a second pane dialling the box
459 # would put a dial in the log before the one under test.
460 askpass_wall "$ASKSTATE_PW" "askbox-pw@127.0.0.1"
451 : > "$ASKDIALLOG" 461 : > "$ASKDIALLOG"
452 : > "$ASKPROMPTLOG" 462 : > "$ASKPROMPTLOG"
453 set +e 463 set +e
@@ -461,7 +471,7 @@ send \x1cs
461 expect unreachable: 15000 471 expect unreachable: 15000
462 send 2 472 send 2
463 settle 400 15000 473 settle 400 15000
464 send \r 474 send c
465 settle 2500 20000 475 settle 2500 20000
466 send \x1cd 476 send \x1cd
467 waitexit 10000 477 waitexit 10000
test/e2e_lib.sh
Old New
@@ -544,6 +544,34 @@ no_saved_tree() {
544 rm -f "$1/mux/layout" 544 rm -f "$1/mux/layout"
545 } 545 }
546 546
547 # seed_layout STATE ORIENT SPELLING... — the wall a `mux` under STATE opens.
548 #
549 # The layout file IS the wall: the poll grades panes and adds none, so a
550 # session nobody wrote into this file is on no wall at all. A leg that used
551 # to get its tiles from a daemon's list therefore has to AUTHOR them, and
552 # this is the one place that spells the grammar — `mux-layout 1`, then a
553 # single `leaf` or an ORIENT container with one indented `leaf` per
554 # spelling. Every leaf weighs 1, which is an equal cut: a leg that cares
555 # about the weights writes the file itself and says why.
556 #
557 # A SPELLING is `HOST#SESSION` where HOST is the hosts file's own line
558 # (`--sock PATH`, `box`, `quic://box`) byte for byte — the seed matches
559 # leaves against host spellings and refuses the whole file over one that
560 # names no listed daemon.
561 seed_layout() {
562 _sl_state="$1"; _sl_orient="$2"; shift 2
563 mkdir -p "$_sl_state/mux"
564 {
565 printf 'mux-layout 1\n'
566 if [ "$#" -eq 1 ]; then
567 printf 'leaf 1 %s\n' "$1"
568 else
569 printf '%s 0\n' "$_sl_orient"
570 for _sl in "$@"; do printf ' leaf 1 %s\n' "$_sl"; done
571 fi
572 } > "$_sl_state/mux/layout"
573 }
574
547 # await_repaint FILE BASELINE LABEL — wait until repaints FILE exceeds 575 # await_repaint FILE BASELINE LABEL — wait until repaints FILE exceeds
548 # BASELINE (5s, scaled), FAIL with the capture otherwise. 576 # BASELINE (5s, scaled), FAIL with the capture otherwise.
549 await_repaint() { 577 await_repaint() {
@@ -946,19 +974,28 @@ assert_ws_converged() {
946 } 974 }
947 975
948 # tile_id_of ORIGIN LABEL SESSION — the hub's id for one tile of `GET 976 # tile_id_of ORIGIN LABEL SESSION — the hub's id for one tile of `GET
949 # /tiles`, or empty. Ids are BIRTH order and one poller thread per host 977 # /tiles`, or empty. Ids are BIRTH order and a birth can come from the
950 # races the others, so no leg may predict them; the label AND the session 978 # file, from the page's `+` or from a restart, so no leg may predict them;
951 # together are what names a tile, because a daemon with two sessions wears 979 # the label AND the session together are what names a tile, because a
952 # one label twice. 980 # daemon with two sessions wears one label twice.
981 #
982 # Matched up to the comma after the session and not to the object's `}`:
983 # `state` follows the session now, so a pattern anchored on the brace
984 # would find nothing at all — and the comma is what keeps `1` from
985 # matching `11`.
953 tile_id_of() { 986 tile_id_of() {
954 curl -s "$1/tiles" | tr '{' '\n' | 987 curl -s "$1/tiles" | tr '{' '\n' |
955 grep -F "\"label\":\"$2\",\"session\":\"$3\"}" | 988 grep -F "\"label\":\"$2\",\"session\":\"$3\"," |
956 sed -n 's/^"id":\([0-9][0-9]*\),.*/\1/p' | head -1 989 sed -n 's/^"id":\([0-9][0-9]*\),.*/\1/p' | head -1
957 } 990 }
958 991
959 # tiles_shape ORIGIN — `GET /tiles` with the ids struck out: the ORDER and 992 # tiles_shape ORIGIN — `GET /tiles` with the ids and the states struck
960 # the labels are a leg's to assert, the numbering is not. 993 # out: the ORDER and the labels are a leg's to assert, the numbering is
961 tiles_shape() { curl -s "$1/tiles" | sed 's/"id":[0-9]*,//g'; } 994 # not, and neither is a grade that changes with every poll. A leg whose
995 # claim IS the grade greps the raw answer for it.
996 tiles_shape() {
997 curl -s "$1/tiles" | sed 's/"id":[0-9]*,//g; s/,"state":"[a-z]*"//g'
998 }
962 999
963 # Scenario checkpoints. The suite's final line asserts the COUNT of these 1000 # Scenario checkpoints. The suite's final line asserts the COUNT of these
964 # against a literal: adding or removing a scenario means updating that 1001 # against a literal: adding or removing a scenario means updating that