13e66d44
test: the e2e suite pins the wall as the layout
a73x 2026-09-03 05:20
Commit message
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 |