ec5c8b51
test: 09_wallhist moves to a file of its own
a73x 2026-08-26 19:07
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -102,7 +102,7 @@ done | |||
| 102 | E2E_DIR=$(dirname "$0") | 102 | E2E_DIR=$(dirname "$0") |
| 103 | . "$E2E_DIR/e2e_lib.sh" | 103 | . "$E2E_DIR/e2e_lib.sh" |
| 104 | 104 | ||
| 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse' | 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_wallhist' |
| 106 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses | 106 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses |
| 107 | # these by name rather than running them: a group whose fixtures are | 107 | # these by name rather than running them: a group whose fixtures are |
| 108 | # missing does not fail, it passes having tested something else — and a | 108 | # missing does not fail, it passes having tested something else — and a |
| @@ -145,639 +145,6 @@ for _g in $E2E_GROUPS; do | |||
| 145 | . "$E2E_DIR/e2e_$_g.sh" | 145 | . "$E2E_DIR/e2e_$_g.sh" |
| 146 | done | 146 | done |
| 147 | 147 | ||
| 148 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its | ||
| 149 | # own, for the dynamic-wall leg's reason turned up one notch: what these | ||
| 150 | # two blocks read back is the wall FILE, and every `mux` in this suite now | ||
| 151 | # writes a tile into the shared $XDG_STATE_HOME — so a file read out of | ||
| 152 | # that one would be every other block's attaches as much as this one's. | ||
| 153 | # The daemon is separate because the tile spelling contains the socket | ||
| 154 | # path, which is what the assertions grep for. | ||
| 155 | SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock" | ||
| 156 | defer_sock "$SOCK40" | ||
| 157 | WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$" | ||
| 158 | defer_rm "$WHSTATE" | ||
| 159 | WHWALL="$WHSTATE/mux/wall" | ||
| 160 | # A state home whose wall FILE is a directory: the unwritable case, which | ||
| 161 | # has to warn and let the attach happen anyway. | ||
| 162 | WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$" | ||
| 163 | defer_rm "$WHBAD" | ||
| 164 | # And a third, for the `x` block: its wall is built by two attaches and | ||
| 165 | # then eaten by `x`, so it must start empty and stay its own. | ||
| 166 | WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$" | ||
| 167 | defer_rm "$WHXSTATE" | ||
| 168 | # The convergence block: `mux TARGET` is a wall of one tile whose rect is | ||
| 169 | # the whole terminal. Three daemons and three state homes, each of them its own | ||
| 170 | # for the same reason the attach-history block's is — what these legs read | ||
| 171 | # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the | ||
| 172 | # suite has ever attached to. | ||
| 173 | SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock" | ||
| 174 | defer_sock "$SOCK45" | ||
| 175 | CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$" | ||
| 176 | defer_rm "$CVSTATE" | ||
| 177 | CVWALL="$CVSTATE/mux/wall" | ||
| 178 | # The ring-grows-the-wall leg needs TWO state homes: one for the client | ||
| 179 | # under test, and one for the setup client whose attach must NOT leave a | ||
| 180 | # tile the leg would then find already there. That absence is the case | ||
| 181 | # under test — a sibling the daemon knows and the wall does not. | ||
| 182 | SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock" | ||
| 183 | defer_sock "$SOCK46" | ||
| 184 | RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$" | ||
| 185 | defer_rm "$RGSTATE" | ||
| 186 | RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$" | ||
| 187 | defer_rm "$RGOTHER" | ||
| 188 | # The exit-semantics leg: a session ending in the focused tile. | ||
| 189 | SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" | ||
| 190 | defer_sock "$SOCK47" | ||
| 191 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" | ||
| 192 | defer_rm "$XESTATE" | ||
| 193 | |||
| 194 | # ---- the wall is attach HISTORY ----------------------------------------- | ||
| 195 | # | ||
| 196 | # The mechanical rule (wall-home-screen spec, phase 2): an attach that | ||
| 197 | # CLAIMS the grid writes its tile; one that does not, does not. Four | ||
| 198 | # witnesses, in one state home nothing else writes to: | ||
| 199 | # | ||
| 200 | # * a `mux` attach adds exactly one line, spelled `#0` — the RESOLVED | ||
| 201 | # default name, because a wall line has to be a spelling a user could | ||
| 202 | # type back, not the empty wire name the attach frame carries; | ||
| 203 | # * a second attach to the same session adds nothing (dedup, byte-exact); | ||
| 204 | # * `muxa` drives the SAME session and the file is byte-identical after | ||
| 205 | # — the 0x0 invariant, made testable rather than merely stated. The | ||
| 206 | # hash is anchored on a marker the agent actually landed, so a muxa | ||
| 207 | # that did nothing at all cannot pass this leg by doing nothing; | ||
| 208 | # * a session reached by the `Ctrl-\ c` chord earns its own tile, which | ||
| 209 | # is the "the wall grows by the truth" clause — those re-dials happen | ||
| 210 | # inside client.attach's own loop and never come back through main. | ||
| 211 | # | ||
| 212 | # Then the two failure faces: an unwritable wall file warns and does NOT | ||
| 213 | # stop the attach, and 'mux wall add' refuses a bad spelling by name | ||
| 214 | # without touching the file. | ||
| 215 | start_daemon "$SOCK40" "$OUT.wh.d" "attach-history daemon never bound" --shell /bin/sh | ||
| 216 | D37PID=$DPID | ||
| 217 | |||
| 218 | pipe_mux "$OUT.wh1" "$OUT.wh1.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 219 | pipe_send 'printf "wh1-%%s\\n" pin\n' | ||
| 220 | await_out "$OUT.wh1" "wh1-pin" "wh1-pin never reached the client" | ||
| 221 | pipe_detach | ||
| 222 | wait_grid "$SOCK40" "wh1-pin" "attach history: the default session's marker" | ||
| 223 | grep -qx -- "--sock $SOCK40#0" "$WHWALL" || { | ||
| 224 | echo "e2e FAIL: attach history: the attach wrote no tile (or not '#0'):" | ||
| 225 | cat "$WHWALL" 2>&1; exit 1; } | ||
| 226 | _wh_n=$(wc -l < "$WHWALL") | ||
| 227 | [ "$_wh_n" -eq 1 ] || { | ||
| 228 | echo "e2e FAIL: attach history: one attach wrote $_wh_n lines:" | ||
| 229 | cat "$WHWALL"; exit 1; } | ||
| 230 | |||
| 231 | pipe_mux "$OUT.wh2" "$OUT.wh2.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 232 | pipe_send 'printf "wh2-%%s\\n" pin\n' | ||
| 233 | await_out "$OUT.wh2" "wh2-pin" "wh2-pin never reached the client" | ||
| 234 | pipe_detach | ||
| 235 | wait_grid "$SOCK40" "wh2-pin" "attach history: the second attach's marker" | ||
| 236 | _wh_n=$(wc -l < "$WHWALL") | ||
| 237 | [ "$_wh_n" -eq 1 ] || { | ||
| 238 | echo "e2e FAIL: attach history: a second attach to the same session made" | ||
| 239 | echo " $_wh_n lines — dedup is not deduping:" | ||
| 240 | cat "$WHWALL"; exit 1; } | ||
| 241 | |||
| 242 | # muxa: the 0x0 invariant, as a hash. Its marker is the anchor — a muxa | ||
| 243 | # that never attached would leave the file identical too, and this leg | ||
| 244 | # would pass while proving nothing. | ||
| 245 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 246 | XDG_STATE_HOME="$WHSTATE" "$MUXA" send 'printf "whagent-%s\n" pin\n' \ | ||
| 247 | --sock "$SOCK40" > "$OUT.whb" 2> "$OUT.whb.err" | ||
| 248 | wait_grid "$SOCK40" "whagent-pin" "attach history: the agent's own marker" | ||
| 249 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 250 | echo "e2e FAIL: attach history: muxa attached at 0x0 and still wrote a tile" | ||
| 251 | echo " — 'muxa never touches the wall' is broken:" | ||
| 252 | cat "$WHWALL"; exit 1; } | ||
| 253 | |||
| 254 | # Ctrl-\ c: the chord re-dials full size, so the session it creates earns a | ||
| 255 | # tile like any other attach. The chord gets a write of its own — a chord | ||
| 256 | # ends its chunk, and a piped client reads chunks. | ||
| 257 | { sleep 1; printf '\034c'; sleep 3; printf 'printf "whc-%%s\\n" pin\n'; \ | ||
| 258 | sleep 2; printf '\034\034'; } | \ | ||
| 259 | XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" \ | ||
| 260 | > "$OUT.whc" 2> "$OUT.whc.err" | ||
| 261 | wait_grid "$SOCK40" "whc-pin" "attach history: the chord-created session's marker" 1 | ||
| 262 | grep -qx -- "--sock $SOCK40#1" "$WHWALL" || { | ||
| 263 | echo "e2e FAIL: attach history: a session visited by Ctrl-\\ c earned no tile:" | ||
| 264 | cat "$WHWALL"; cat "$OUT.whc.err"; exit 1; } | ||
| 265 | |||
| 266 | # An unwritable wall file: a DIRECTORY where the file belongs. One warning, | ||
| 267 | # and an attach that happened anyway — best effort means the attach is the | ||
| 268 | # act and the tile is only the record. | ||
| 269 | mkdir -p "$WHBAD/mux/wall" | ||
| 270 | pipe_mux "$OUT.whro" "$OUT.whro.err" env XDG_STATE_HOME="$WHBAD" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 271 | pipe_send 'printf "whro-%%s\\n" pin\n' | ||
| 272 | await_out "$OUT.whro" "whro-pin" "whro-pin never reached the client" | ||
| 273 | pipe_detach | ||
| 274 | grep -q "wall not updated" "$OUT.whro.err" || { | ||
| 275 | echo "e2e FAIL: attach history: an unwritable wall file said nothing:" | ||
| 276 | cat "$OUT.whro.err"; exit 1; } | ||
| 277 | wait_grid "$SOCK40" "whro-pin" "attach history: the attach an unwritable wall did not block" | ||
| 278 | |||
| 279 | # The self-attach refusal exits BEFORE the dial, so it must exit before the | ||
| 280 | # write too: a tile is the record of an attach, and this one never happened. | ||
| 281 | # Spelled with the environment rather than a session shell because that is | ||
| 282 | # all the refusal reads — and it keeps the assertion a hash of one file | ||
| 283 | # rather than a search for a line other legs also write. | ||
| 284 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 285 | set +e | ||
| 286 | MUX_SOCK="$SOCK40" MUX_SESSION=0 XDG_STATE_HOME="$WHSTATE" \ | ||
| 287 | "$MUX" --sock "$SOCK40" > "$OUT.whself" 2>&1 | ||
| 288 | RC=$? | ||
| 289 | set -e | ||
| 290 | [ "$RC" -eq 2 ] || { | ||
| 291 | echo "e2e FAIL: attach history: the self-attach refusal exited $RC, want 2:" | ||
| 292 | cat "$OUT.whself"; exit 1; } | ||
| 293 | grep -q "this shell is inside that session" "$OUT.whself" || { | ||
| 294 | echo "e2e FAIL: attach history: the self-attach leg refused for another reason:" | ||
| 295 | cat "$OUT.whself"; exit 1; } | ||
| 296 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 297 | echo "e2e FAIL: attach history: a REFUSED self-attach still wrote a tile:" | ||
| 298 | cat "$WHWALL"; exit 1; } | ||
| 299 | |||
| 300 | # 'mux wall add': validation at ADD time, in the grammar's own words, and | ||
| 301 | # a refused spelling writes nothing at all. | ||
| 302 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 303 | set +e | ||
| 304 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall add "box#has space" > "$OUT.whadd" 2>&1 | ||
| 305 | RC=$? | ||
| 306 | set -e | ||
| 307 | [ "$RC" -ne 0 ] || { | ||
| 308 | echo "e2e FAIL: attach history: 'mux wall add' accepted a bad session name"; exit 1; } | ||
| 309 | grep -q "bad session name" "$OUT.whadd" || { | ||
| 310 | echo "e2e FAIL: attach history: a refused add never named its reason:" | ||
| 311 | cat "$OUT.whadd"; exit 1; } | ||
| 312 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 313 | echo "e2e FAIL: attach history: a refused add still edited the file:" | ||
| 314 | cat "$WHWALL"; exit 1; } | ||
| 315 | |||
| 316 | # ...and the pair that works, including `rm` of what is not there. | ||
| 317 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall add "whbox#work" > "$OUT.whadd" 2>&1 | ||
| 318 | grep -qx "whbox#work" "$WHWALL" || { | ||
| 319 | echo "e2e FAIL: attach history: 'mux wall add' added nothing:"; cat "$WHWALL"; exit 1; } | ||
| 320 | set +e | ||
| 321 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall rm "whbox#work" > "$OUT.whrm" 2>&1 | ||
| 322 | RC=$? | ||
| 323 | set -e | ||
| 324 | [ "$RC" -eq 0 ] || { | ||
| 325 | echo "e2e FAIL: attach history: 'mux wall rm' exited $RC:"; cat "$OUT.whrm"; exit 1; } | ||
| 326 | if grep -q "whbox#work" "$WHWALL"; then | ||
| 327 | echo "e2e FAIL: attach history: 'mux wall rm' left the line behind:" | ||
| 328 | cat "$WHWALL"; exit 1 | ||
| 329 | fi | ||
| 330 | set +e | ||
| 331 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall rm "whbox#work" > "$OUT.whrm" 2>&1 | ||
| 332 | RC=$? | ||
| 333 | set -e | ||
| 334 | [ "$RC" -ne 0 ] || { | ||
| 335 | echo "e2e FAIL: attach history: removing an absent tile exited 0"; exit 1; } | ||
| 336 | grep -q "not on the wall" "$OUT.whrm" || { | ||
| 337 | echo "e2e FAIL: attach history: an absent rm never said so:"; cat "$OUT.whrm"; exit 1; } | ||
| 338 | ok "the wall is attach history: mux adds, muxa never does, add/rm edit it" | ||
| 339 | |||
| 340 | # ---- `x` forgets a tile, and never kills its session -------------------- | ||
| 341 | # | ||
| 342 | # The wall this leg puts up is one nothing typed by hand: two attaches | ||
| 343 | # BUILT it, which is the model's whole claim. Then `Ctrl-\ x` is asked for | ||
| 344 | # the two things it must do and the one it must not — remove the line, | ||
| 345 | # re-cut the wall, and leave the session running ("remove is detach"). | ||
| 346 | # | ||
| 347 | # Two ptyclient runs rather than one, because the file is the artifact: | ||
| 348 | # what a run did is only readable after it has exited, so the one-tile | ||
| 349 | # state between them is the second run's input. | ||
| 350 | pipe_mux "$OUT.whxa" "$OUT.whxa.err" env XDG_STATE_HOME="$WHXSTATE" timeout 40 "$MUX" --sock "$SOCK40" --session xa | ||
| 351 | pipe_send 'printf "whxa-%%s\\n" pin\n' | ||
| 352 | await_out "$OUT.whxa" "whxa-pin" "whxa-pin never reached the client" | ||
| 353 | pipe_detach | ||
| 354 | wait_grid "$SOCK40" "whxa-pin" "x forgets: session xa's marker" xa | ||
| 355 | pipe_mux "$OUT.whxb" "$OUT.whxb.err" env XDG_STATE_HOME="$WHXSTATE" timeout 40 "$MUX" --sock "$SOCK40" --session xb | ||
| 356 | pipe_send 'printf "whxb-%%s\\n" pin\n' | ||
| 357 | await_out "$OUT.whxb" "whxb-pin" "whxb-pin never reached the client" | ||
| 358 | pipe_detach | ||
| 359 | wait_grid "$SOCK40" "whxb-pin" "x forgets: session xb's marker" xb | ||
| 360 | _whx_n=$(wc -l < "$WHXSTATE/mux/wall") | ||
| 361 | [ "$_whx_n" -eq 2 ] || { | ||
| 362 | echo "e2e FAIL: x forgets: two attaches built a wall of $_whx_n tiles:" | ||
| 363 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 364 | |||
| 365 | # No operands: the wall this shows is the file, which is the point. The | ||
| 366 | # state home rides on PTYCLIENT itself and is inherited by the child — | ||
| 367 | # ptyclient execs its `--` argv as given, so an `env` wrapper in front of | ||
| 368 | # `mux` is not a command it runs, it is a program it fails to find. | ||
| 369 | # | ||
| 370 | # ONE marker expected, then `settle` — the pattern every other two-tile | ||
| 371 | # ptyclient leg here uses, and for a reason review reproduced on this | ||
| 372 | # suite: the tiles are independent threads, `expect` consumes forward, and | ||
| 373 | # whichever stripe painted second leaves the other one's marker BEHIND the | ||
| 374 | # cursor. Two consecutive expects against two pumps is a coin flip that | ||
| 375 | # spends its whole budget waiting for bytes that already went past. What | ||
| 376 | # the `Ctrl-\ x` below needs is a wall that is up and settled, which the | ||
| 377 | # single expect plus `settle` gives; the tile it forgets is the FOCUSED | ||
| 378 | # one, tile 0, whose line the file is then asserted NOT to hold. | ||
| 379 | set +e | ||
| 380 | XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 381 | --out "$OUT.whxcap" --err "$OUT.whxcap.err" -- \ | ||
| 382 | "$MUX" wall > "$OUT.whxpc" 2>&1 <<'EOF' | ||
| 383 | expect whxb-pin 20000 | ||
| 384 | settle 800 20000 | ||
| 385 | send \x1cx | ||
| 386 | settle 800 20000 | ||
| 387 | send \x1cd | ||
| 388 | waitexit 10000 | ||
| 389 | EOF | ||
| 390 | RC=$? | ||
| 391 | set -e | ||
| 392 | [ "$RC" -eq 0 ] || { | ||
| 393 | echo "e2e FAIL: x forgets: ptyclient leg exited $RC (did \\x1cx forget the tile?):" | ||
| 394 | cat "$OUT.whxpc"; exit 1; } | ||
| 395 | # The line is gone and the survivor kept its place — `Ctrl-\ x` is an | ||
| 396 | # ordered removal, not a rewrite of the wall. | ||
| 397 | _whx_n=$(wc -l < "$WHXSTATE/mux/wall") | ||
| 398 | [ "$_whx_n" -eq 1 ] || { | ||
| 399 | echo "e2e FAIL: x forgets: the wall file holds $_whx_n lines after one \\x1cx:" | ||
| 400 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 401 | grep -qx -- "--sock $SOCK40#xb" "$WHXSTATE/mux/wall" || { | ||
| 402 | echo "e2e FAIL: x forgets: the wrong line survived:" | ||
| 403 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 404 | # The survivor took the whole terminal: its marker painted a SECOND time | ||
| 405 | # after the re-cut, counted because one paint is the wall's first draw and | ||
| 406 | # says nothing about the hole closing around the survivor. | ||
| 407 | _whx_bars=$(grep -o "whxb-pin" "$OUT.whxcap" | wc -l) | ||
| 408 | [ "$_whx_bars" -ge 2 ] || { | ||
| 409 | echo "e2e FAIL: x forgets: the surviving tile's marker painted $_whx_bars time(s)," | ||
| 410 | echo " so the wall was never re-cut around the hole:" | ||
| 411 | cat "$OUT.whxpc"; exit 1; } | ||
| 412 | # ...and the SESSION is untouched: it still answers, and it still holds | ||
| 413 | # what it held. "Remove is detach" — the tile went, the session did not. | ||
| 414 | # xa was 80-wide from its pipe attach; this 40-wide wall resized it on | ||
| 415 | # hydrate, and the detach left that size behind. | ||
| 416 | XDG_STATE_HOME="$WHSTATE" "$MUXA" status --sock "$SOCK40" --session xa > "$OUT.whxst" 2>&1 | ||
| 417 | grep -q '"cols":40' "$OUT.whxst" || { | ||
| 418 | echo "e2e FAIL: x forgets: session xa stopped answering — \\x1cx killed it:" | ||
| 419 | cat "$OUT.whxst"; exit 1; } | ||
| 420 | wait_grid "$SOCK40" "whxa-pin" "x forgets: xa's grid outlived its tile" xa | ||
| 421 | |||
| 422 | # The last tile: an empty wall SAYS so rather than going blank. | ||
| 423 | set +e | ||
| 424 | XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 425 | --out "$OUT.whxcap2" --err "$OUT.whxcap2.err" -- \ | ||
| 426 | "$MUX" wall > "$OUT.whxpc2" 2>&1 <<'EOF' | ||
| 427 | expect whxb-pin 20000 | ||
| 428 | settle 800 20000 | ||
| 429 | send \x1cx | ||
| 430 | settle 800 20000 | ||
| 431 | send \x1cd | ||
| 432 | waitexit 10000 | ||
| 433 | EOF | ||
| 434 | RC=$? | ||
| 435 | set -e | ||
| 436 | [ "$RC" -eq 0 ] || { | ||
| 437 | echo "e2e FAIL: x forgets: the last-tile leg exited $RC:" | ||
| 438 | cat "$OUT.whxpc2"; exit 1; } | ||
| 439 | grep -q "nothing left to show" "$OUT.whxcap2" || { | ||
| 440 | echo "e2e FAIL: x forgets: forgetting the last tile left a blank terminal" | ||
| 441 | echo " with nothing said:"; cat "$OUT.whxpc2"; exit 1; } | ||
| 442 | [ ! -s "$WHXSTATE/mux/wall" ] || { | ||
| 443 | echo "e2e FAIL: x forgets: the last \\x1cx left lines behind:" | ||
| 444 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 445 | # The phantom tile, while this daemon is FULL: this leg's own four (0, 1, | ||
| 446 | # xa, xb) plus the 28 filled here are max_sessions, so a FIRST attach to | ||
| 447 | # one more name is refused — and a refusal must leave no line behind. Its | ||
| 448 | # state home is neither $WHSTATE nor $WHXSTATE, both of whose walls are | ||
| 449 | # asserted on. This is the case that made the seam move off "the dial | ||
| 450 | # succeeded" — a dial that comes up is not an attach that landed, and only | ||
| 451 | # a SWITCH's refusal ever had somewhere to undo the write from. The leg | ||
| 452 | # cannot pass vacuously: a daemon with room would accept the attach and | ||
| 453 | # the rc check below would fail loudly. | ||
| 454 | fill_sessions "$SOCK40" "$OUT.whfill" fill 4 31 | ||
| 455 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 456 | set +e | ||
| 457 | { sleep 1; printf '\034\034'; } | XDG_STATE_HOME="$WHSTATE" timeout 40 \ | ||
| 458 | "$MUX" --sock "$SOCK40" --session phantom > "$OUT.whph" 2>&1 | ||
| 459 | RC=$? | ||
| 460 | set -e | ||
| 461 | [ "$RC" -ne 0 ] || { | ||
| 462 | echo "e2e FAIL: x forgets: a 33rd session was created on a full daemon," | ||
| 463 | echo " so the phantom-tile leg proved nothing:"; cat "$OUT.whph"; exit 1; } | ||
| 464 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 465 | echo "e2e FAIL: x forgets: a REFUSED first attach recorded a phantom tile:" | ||
| 466 | cat "$WHWALL"; exit 1; } | ||
| 467 | assert_stopped "$SOCK40" "$D37PID" "x forgets" "$OUT.whstop" | ||
| 468 | D37PID="" | ||
| 469 | ok "\\x1cx forgets a tile and leaves its session running; a refusal records nothing" | ||
| 470 | |||
| 471 | # ---- `mux TARGET` is a wall of one tile --------------------------------- | ||
| 472 | # | ||
| 473 | # The convergence's headline: `mux` dials and attaches exactly as it always | ||
| 474 | # did, and what it runs is the WALL — one tile whose rect is the whole | ||
| 475 | # terminal, no label bar, byte-identical to a plain client. `Ctrl-\ w` | ||
| 476 | # folds the SAVED wall into this one: it hydrates the file's tiles beside | ||
| 477 | # the tile this run is standing on, and what appears is THE wall — this | ||
| 478 | # run's tile plus the ones the saved file already holds. | ||
| 479 | # | ||
| 480 | # Four claims, and each needs a different kind of witness: | ||
| 481 | # | ||
| 482 | # * the wall `Ctrl-\ w` folds in is the SAVED one. A tile written by an | ||
| 483 | # EARLIER `mux` (the `#side` attach below, into this leg's own state | ||
| 484 | # home) paints a stripe here, in a process that was never told about | ||
| 485 | # it on its command line. A wall that only ever showed this run's own | ||
| 486 | # tile would satisfy every other assertion in this leg. | ||
| 487 | # * the focus stays on the entry tile, and that costs NOTHING at the | ||
| 488 | # daemon. The attach counter is sampled across the fold and the | ||
| 489 | # injection and moves only for the two real attaches (the entry tile | ||
| 490 | # and the saved tile the fold hydrated): focus is client-local and | ||
| 491 | # sends nothing on the wire. | ||
| 492 | # * `Ctrl-\ d` leaves MUX. There is no child and no second screen; the | ||
| 493 | # one-tile wall is the program, and the detach chord is the way out. | ||
| 494 | # * ...and the attach that opened all this earned exactly one line in the | ||
| 495 | # file, spelled `#0`, beside the `#side` that was already there. | ||
| 496 | # | ||
| 497 | # A state home of its own: the wall `Ctrl-\ w` folds in is the saved one, | ||
| 498 | # and the shared $XDG_STATE_HOME is every socket the suite has ever | ||
| 499 | # attached to. | ||
| 500 | start_daemon "$SOCK45" "$OUT.cv.d" "convergence daemon never bound" --shell /bin/sh | ||
| 501 | D45PID=$DPID | ||
| 502 | |||
| 503 | # The tile that is already on the wall before this leg's client starts. A | ||
| 504 | # piped `mux`, so its attach writes the line and then goes: what the leg | ||
| 505 | # asserts on is the FILE it left behind, not a process still holding it. | ||
| 506 | pipe_mux "$OUT.cvside" "$OUT.cvside.err" env XDG_STATE_HOME="$CVSTATE" timeout 40 "$MUX" --sock "$SOCK45" --session side | ||
| 507 | pipe_send 'printf "cvside-%%s\\n" pin\n' | ||
| 508 | await_out "$OUT.cvside" "cvside-pin" "cvside-pin never reached the client" | ||
| 509 | pipe_detach | ||
| 510 | wait_grid "$SOCK45" "cvside-pin" "convergence: the saved tile's marker" side | ||
| 511 | grep -qx -- "--sock $SOCK45#side" "$CVWALL" || { | ||
| 512 | echo "e2e FAIL: convergence: the setup attach wrote no tile to fold in:" | ||
| 513 | cat "$CVWALL" 2>&1; exit 1; } | ||
| 514 | |||
| 515 | # The whole run's budget, and every one of the three is named: the entry | ||
| 516 | # tile's attach, the saved tile's when the fold hydrates it, and the | ||
| 517 | # `muxa send` below, which is an attach like any other. The fold itself | ||
| 518 | # is a real attach (it dials the saved tile's session); the focus move and | ||
| 519 | # the leave add nothing, and a cumulative counter is what says so — a | ||
| 520 | # gauge would let a dial that opened and closed between two samples hide | ||
| 521 | # inside it. | ||
| 522 | CVATT_BEFORE=$(attaches_now "$SOCK45") | ||
| 523 | ( sleep 6; "$MUXA" send 'printf "cvret-%s\n" pin\n' \ | ||
| 524 | --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) & | ||
| 525 | CVINJPID=$! | ||
| 526 | defer_kill "$CVINJPID" | ||
| 527 | set +e | ||
| 528 | # tall: Ctrl-\ w folds in a saved tile, making two at 100x30 (100 >= 60); | ||
| 529 | # 70x36 stays stacked (70 < 72) and fits the bar label the assertion greps | ||
| 530 | XDG_STATE_HOME="$CVSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \ | ||
| 531 | --out "$OUT.cvcap" --err "$OUT.cvcap.err" \ | ||
| 532 | -- "$MUX" --sock "$SOCK45" > "$OUT.cvpc" 2>&1 <<'EOF' | ||
| 533 | expect \x1b[?1049h 15000 | ||
| 534 | settle 400 15000 | ||
| 535 | send printf 'cvhome-%s\\n' pin\n | ||
| 536 | expect cvhome-pin 15000 | ||
| 537 | settle 400 15000 | ||
| 538 | send \x1cw | ||
| 539 | expect cvside-pin 20000 | ||
| 540 | settle 900 20000 | ||
| 541 | expect cvret-pin 20000 | ||
| 542 | settle 500 15000 | ||
| 543 | send \x1cd | ||
| 544 | waitexit 10000 | ||
| 545 | EOF | ||
| 546 | RC=$? | ||
| 547 | set -e | ||
| 548 | wait "$CVINJPID" 2>/dev/null || true | ||
| 549 | [ "$RC" -eq 0 ] || { | ||
| 550 | echo "e2e FAIL: convergence: ptyclient leg exited $RC (did \\x1cw fold, did \\x1cd leave?):" | ||
| 551 | cat "$OUT.cvpc"; cat "$OUT.cvinj" 2>/dev/null; exit 1; } | ||
| 552 | # The saved tile really painted a STRIPE, which only a wall draws — a bar | ||
| 553 | # with a spelling and a state word in it. A bare marker grep would be | ||
| 554 | # satisfied by the setup attach's own capture bleeding through. | ||
| 555 | grep -q -- "--sock $SOCK45#side \[up\]" "$OUT.cvcap" || { | ||
| 556 | echo "e2e FAIL: convergence: the saved wall's tile never became a stripe:" | ||
| 557 | cat "$OUT.cvpc"; exit 1; } | ||
| 558 | grep -q -- "--sock $SOCK45#0 \[up\]" "$OUT.cvcap" || { | ||
| 559 | echo "e2e FAIL: convergence: the attached tile never became a stripe:" | ||
| 560 | cat "$OUT.cvpc"; exit 1; } | ||
| 561 | # The fold was a real attach (the saved tile dialled) and the injection was | ||
| 562 | # another; the focus move and the leave added nothing. THREE is the whole | ||
| 563 | # budget — entry tile, hydrated saved tile, injector — and a focus that | ||
| 564 | # re-dialled would make it four. | ||
| 565 | CVATT=$(attaches_now "$SOCK45") | ||
| 566 | assert_attach_delta "$CVATT_BEFORE" "$CVATT" 3 "convergence" | ||
| 567 | # The file: one line for the attach this run made, beside the one it found. | ||
| 568 | _cv_n=$(wc -l < "$CVWALL") | ||
| 569 | [ "$_cv_n" -eq 2 ] || { | ||
| 570 | echo "e2e FAIL: convergence: the wall file holds $_cv_n lines, want 2:" | ||
| 571 | cat "$CVWALL"; exit 1; } | ||
| 572 | grep -qx -- "--sock $SOCK45#0" "$CVWALL" || { | ||
| 573 | echo "e2e FAIL: convergence: the attach earned no tile of its own:" | ||
| 574 | cat "$CVWALL"; exit 1; } | ||
| 575 | assert_stopped "$SOCK45" "$D45PID" "convergence" "$OUT.cvstop" | ||
| 576 | D45PID="" | ||
| 577 | ok "mux is the wall: Ctrl-\\ w folds in the saved tiles, the focus stays put, Ctrl-\\ d leaves" | ||
| 578 | |||
| 579 | # ---- the ring GROWS the wall, and only when it has to -------------------- | ||
| 580 | # | ||
| 581 | # The spec's `n`/`p` rule, which phase 3c is where it lands: those chords | ||
| 582 | # move the focus around the DAEMON'S session ring, not around the wall's | ||
| 583 | # tiles. A sibling that already has a tile is an instant focus move; one | ||
| 584 | # that does not GETS one, because visiting it is an attach and attach adds. | ||
| 585 | # | ||
| 586 | # The leg is built so the two halves are the SAME chord pressed twice, and | ||
| 587 | # the difference between them is the whole assertion: | ||
| 588 | # | ||
| 589 | # * `\x1cn` from session 0 lands on `two`, which this run has never seen | ||
| 590 | # — its tile was written into a DIFFERENT state home by the setup | ||
| 591 | # client, so the daemon knows the session and this wall does not. One | ||
| 592 | # dial, one attach, one new line in the file, and the marker typed | ||
| 593 | # afterwards lands in `two`'s shell. | ||
| 594 | # * `\x1cn` again wraps back to 0, which now HAS a tile. Nothing is | ||
| 595 | # dialled: the counter must not move across that step, and the marker | ||
| 596 | # lands back in session 0. | ||
| 597 | # | ||
| 598 | # The attach budget for the run is therefore exactly 2 (the entry tile and | ||
| 599 | # the tile the first `n` grew), and the second `n` is the one that must be | ||
| 600 | # free. Sampled around the whole ptyclient run rather than around each | ||
| 601 | # step, because a step that dialled and closed between two samples would | ||
| 602 | # hide inside a gauge — the counter is cumulative and cannot. | ||
| 603 | start_daemon "$SOCK46" "$OUT.rg.d" "ring-grow daemon never bound" --shell /bin/sh | ||
| 604 | D46PID=$DPID | ||
| 605 | |||
| 606 | # `two` exists on the DAEMON but not in this leg's wall: a separate state | ||
| 607 | # home is what makes the setup client's own tile invisible here, so the | ||
| 608 | # first `n` meets a sibling with no tile — which is the case under test. | ||
| 609 | pipe_mux "$OUT.rgtwo" "$OUT.rgtwo.err" env XDG_STATE_HOME="$RGOTHER" timeout 40 "$MUX" --sock "$SOCK46" --session two | ||
| 610 | pipe_send 'printf "rgtwo-%%s\\n" pin\n' | ||
| 611 | await_out "$OUT.rgtwo" "rgtwo-pin" "rgtwo-pin never reached the client" | ||
| 612 | pipe_detach | ||
| 613 | wait_grid "$SOCK46" "rgtwo-pin" "ring grow: the untiled sibling's marker" two | ||
| 614 | grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" 2>/dev/null && { | ||
| 615 | echo "e2e FAIL: ring grow: the sibling already had a tile, so the ADD is vacuous:" | ||
| 616 | cat "$RGSTATE/mux/wall"; exit 1; } | ||
| 617 | |||
| 618 | RGATT_BEFORE=$(attaches_now "$SOCK46") | ||
| 619 | set +e | ||
| 620 | # tall: Ctrl-\ n grows the wall to two tiles at 100x30 (100 >= 60) | ||
| 621 | XDG_STATE_HOME="$RGSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 622 | --out "$OUT.rgcap" --err "$OUT.rgcap.err" \ | ||
| 623 | -- "$MUX" --sock "$SOCK46" > "$OUT.rgpc" 2>&1 <<'EOF' | ||
| 624 | expect \x1b[?1049h 15000 | ||
| 625 | settle 400 15000 | ||
| 626 | send printf 'rgzero-%s\\n' pin\n | ||
| 627 | expect rgzero-pin 15000 | ||
| 628 | settle 400 15000 | ||
| 629 | send \x1cn | ||
| 630 | expect rgtwo-pin 20000 | ||
| 631 | settle 500 15000 | ||
| 632 | send printf 'rghere-%s\\n' pin\n | ||
| 633 | expect rghere-pin 15000 | ||
| 634 | settle 400 15000 | ||
| 635 | send \x1cn | ||
| 636 | expect rgzero-pin 20000 | ||
| 637 | settle 500 15000 | ||
| 638 | send printf 'rgback-%s\\n' pin\n | ||
| 639 | expect rgback-pin 15000 | ||
| 640 | settle 400 15000 | ||
| 641 | send \x1c\x1c | ||
| 642 | waitexit 10000 | ||
| 643 | EOF | ||
| 644 | RC=$? | ||
| 645 | set -e | ||
| 646 | RGATT=$(attaches_now "$SOCK46") | ||
| 647 | [ "$RC" -eq 0 ] || { | ||
| 648 | echo "e2e FAIL: ring grow: ptyclient leg exited $RC (did \\x1cn step the ring?):" | ||
| 649 | cat "$OUT.rgpc"; exit 1; } | ||
| 650 | # TWO attaches for the whole run: the entry tile's, and the one the first | ||
| 651 | # `n` grew. A `n` that re-dialled the tile it already had would make it | ||
| 652 | # three, whichever way it closed the old one first. | ||
| 653 | assert_attach_delta "$RGATT_BEFORE" "$RGATT" 2 "ring grow" | ||
| 654 | # The wall grew by the truth: the sibling the ring visited earned a line. | ||
| 655 | grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" || { | ||
| 656 | echo "e2e FAIL: ring grow: the visited sibling earned no tile:" | ||
| 657 | cat "$RGSTATE/mux/wall" 2>&1; exit 1; } | ||
| 658 | _rg_n=$(wc -l < "$RGSTATE/mux/wall") | ||
| 659 | [ "$_rg_n" -eq 2 ] || { | ||
| 660 | echo "e2e FAIL: ring grow: the wall file holds $_rg_n lines, want 2:" | ||
| 661 | cat "$RGSTATE/mux/wall"; exit 1; } | ||
| 662 | # Where the markers landed says the focus really MOVED rather than the wall | ||
| 663 | # having gone on typing at one connection. Each session holds its own and | ||
| 664 | # not its neighbour's. | ||
| 665 | timeout 20 "$MUXA" capture --sock "$SOCK46" --session two > "$OUT.rgcapt" 2>&1 | ||
| 666 | grep -q "rghere-pin" "$OUT.rgcapt" || { | ||
| 667 | echo "e2e FAIL: ring grow: the marker typed after the first n missed session two:" | ||
| 668 | cat "$OUT.rgcapt"; exit 1; } | ||
| 669 | grep -q "rgback-pin" "$OUT.rgcapt" && { | ||
| 670 | echo "e2e FAIL: ring grow: the marker typed after the wrap landed in two:" | ||
| 671 | cat "$OUT.rgcapt"; exit 1; } | ||
| 672 | timeout 20 "$MUXA" capture --sock "$SOCK46" --session 0 > "$OUT.rgcap0" 2>&1 | ||
| 673 | grep -q "rgback-pin" "$OUT.rgcap0" || { | ||
| 674 | echo "e2e FAIL: ring grow: the wrap did not come back to session 0:" | ||
| 675 | cat "$OUT.rgcap0"; exit 1; } | ||
| 676 | assert_stopped "$SOCK46" "$D46PID" "ring grow" "$OUT.rgstop" | ||
| 677 | D46PID="" | ||
| 678 | ok "Ctrl-\\ n grows the wall for a sibling with no tile, and is free for one that has" | ||
| 679 | |||
| 680 | # ---- a session that ends in the focused tile --------------------------- | ||
| 681 | # | ||
| 682 | # The exit rule the convergence had to choose, stated in decisions.md and | ||
| 683 | # pinned here: a session that ends in the focused tile ENDS MUX when it was | ||
| 684 | # the wall's only tile, and LEAVES THE FOCUS TO A NEIGHBOUR when it was not. | ||
| 685 | # | ||
| 686 | # Both halves in one block, because either alone is satisfied by a bug: | ||
| 687 | # | ||
| 688 | # * one tile. `exit 7` in a piped `mux` — the shape every script in the | ||
| 689 | # world uses — and the code comes back 7. Graded against the mutation | ||
| 690 | # (`endAction` dropping to the wall whatever the tile count): `mux` | ||
| 691 | # comes back 0, because the wall it dropped to reads this script's | ||
| 692 | # closed stdin as a wall nobody is left to steer. Zero is not seven and | ||
| 693 | # the leg says so; 124 is the other shape the same bug can take. | ||
| 694 | # * two tiles. `\x1cc` adds one and focuses it, the shell in it exits, and | ||
| 695 | # the tile vanishes — a dead stripe for a cleanly exited shell is noise | ||
| 696 | # — leaving the survivor the whole screen. Then `\x1cd` leaves — exit | ||
| 697 | # 0, not 7 and not the shell's anything, because leaving is the user's | ||
| 698 | # act and not the session's. | ||
| 699 | # * two tiles and a PIPED client whose stdin has already gone. The case | ||
| 700 | # review reproduced as a hang, and the reason the rule's second half is | ||
| 701 | # stated in `endAction` rather than at the read that notices EOF. | ||
| 702 | start_daemon "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound" --shell /bin/sh | ||
| 703 | D47PID=$DPID | ||
| 704 | |||
| 705 | # A wall it dropped to instead would read as 124: it never leaves, because | ||
| 706 | # nothing closes its stdin under it. | ||
| 707 | pipe_mux "$OUT.xe1" "$OUT.xe1.err" env XDG_STATE_HOME="$XESTATE" timeout 40 \ | ||
| 708 | "$MUX" --sock "$SOCK47" | ||
| 709 | pipe_send 'exit 7\n' | ||
| 710 | pipe_waitexit "exit semantics: the only tile's shell exited 7, and mux" 7 | ||
| 711 | |||
| 712 | set +e | ||
| 713 | # tall: Ctrl-\ c creates a second tile at 100x30 (100 >= 60) | ||
| 714 | XDG_STATE_HOME="$XESTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 715 | --out "$OUT.xecap" --err "$OUT.xecap.err" \ | ||
| 716 | -- "$MUX" --sock "$SOCK47" > "$OUT.xepc" 2>&1 <<'EOF' | ||
| 717 | expect \x1b[?1049h 15000 | ||
| 718 | settle 400 15000 | ||
| 719 | send printf 'xehome-%s\\n' pin\n | ||
| 720 | expect xehome-pin 15000 | ||
| 721 | settle 400 15000 | ||
| 722 | send \x1cc | ||
| 723 | expect \x1b[2J 15000 | ||
| 724 | settle 600 15000 | ||
| 725 | send exit 3\n | ||
| 726 | expect xehome-pin 20000 | ||
| 727 | settle 800 20000 | ||
| 728 | send \x1cd | ||
| 729 | waitexit 10000 | ||
| 730 | EOF | ||
| 731 | RC=$? | ||
| 732 | set -e | ||
| 733 | [ "$RC" -eq 0 ] || { | ||
| 734 | echo "e2e FAIL: exit semantics: the multi-tile leg exited $RC (did the focus" | ||
| 735 | echo " move to the neighbour, and did \x1cd leave it?):" | ||
| 736 | cat "$OUT.xepc"; exit 1; } | ||
| 737 | # The exited tile vanished — a dead stripe for a cleanly exited shell is | ||
| 738 | # noise — so there is no `[exited]` bar to read, and the survivor took the | ||
| 739 | # whole screen: a one-tile wall draws no label bar. The marker it repainted | ||
| 740 | # is what `expect xehome-pin` above caught; the grid check below is the | ||
| 741 | # same fact from the daemon's side. | ||
| 742 | # The session that did NOT end is still there: a shell exiting in the | ||
| 743 | # focused tile ends that session and nothing else. | ||
| 744 | wait_grid "$SOCK47" "xehome-pin" "exit semantics: session 0 outlived its neighbour" | ||
| 745 | |||
| 746 | # The third case, and the one review REPRODUCED as a hang: two tiles, but | ||
| 747 | # a PIPED client whose stdin has already closed when the focused session | ||
| 748 | # ends. Moving the focus to a neighbour is not an answer there — a wall | ||
| 749 | # nobody is left to steer has no keyboard, so mux waited in poll forever | ||
| 750 | # with the exit code in hand (RC=124, measured). The rule is the same rule | ||
| 751 | # with its second half stated: an ending nobody can leave is mux's ending. | ||
| 752 | # | ||
| 753 | # The gap is built rather than raced: the shell reads `sleep 4` and | ||
| 754 | # `exit 3` from a stdin that closes IMMEDIATELY after, so the pipe is long | ||
| 755 | # gone by the time the session ends four seconds later. `timeout` is what | ||
| 756 | # turns a regression into a failure instead of a wedged suite. | ||
| 757 | XETIME_START=$(date +%s) | ||
| 758 | set +e | ||
| 759 | { sleep 1; printf '\034c'; sleep 3; printf 'sleep 4\n'; printf 'exit 3\n'; } | \ | ||
| 760 | XDG_STATE_HOME="$XESTATE" timeout 40 "$MUX" --sock "$SOCK47" \ | ||
| 761 | > "$OUT.xe2" 2> "$OUT.xe2.err" | ||
| 762 | RC=$? | ||
| 763 | set -e | ||
| 764 | XETIME=$(( $(date +%s) - XETIME_START )) | ||
| 765 | [ "$RC" -eq 3 ] || { | ||
| 766 | echo "e2e FAIL: exit semantics: a piped two-tile client whose session ended" | ||
| 767 | echo " exited $RC after ${XETIME}s, want 3 — 124 is the wall it" | ||
| 768 | echo " left to a neighbour with nobody left to steer it:" | ||
| 769 | cat "$OUT.xe2.err"; exit 1; } | ||
| 770 | # Promptly, not eventually: the code is right in both readings, but a | ||
| 771 | # client that only exits because the timeout reaped it is the bug wearing | ||
| 772 | # the right answer. The session ends ~8s in; 20 is generous and still far | ||
| 773 | # under the 40s timeout that would otherwise be the only bound. | ||
| 774 | [ "$XETIME" -lt 20 ] || { | ||
| 775 | echo "e2e FAIL: exit semantics: the piped client took ${XETIME}s to notice its" | ||
| 776 | echo " own session had ended — it exited, but it did not notice"; exit 1; } | ||
| 777 | assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop" | ||
| 778 | D47PID="" | ||
| 779 | ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour" | ||
| 780 | |||
| 781 | # Agent forwarding: one socket path, two daemons in turn. The positive leg | 148 | # Agent forwarding: one socket path, two daemons in turn. The positive leg |
| 782 | # ends by typing `exit`, which ends that daemon's only session and takes the | 149 | # ends by typing `exit`, which ends that daemon's only session and takes the |
| 783 | # daemon with it — so the refusal leg below starts its own on the freed path | 150 | # daemon with it — so the refusal leg below starts its own on the freed path |
test/e2e_09_wallhist.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,636 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_09_wallhist.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in | ||
| 3 | # the order they stand in; see the lib's header for what this file may | ||
| 4 | # assume and what it must register. | ||
| 5 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its | ||
| 6 | # own, for the dynamic-wall leg's reason turned up one notch: what these | ||
| 7 | # two blocks read back is the wall FILE, and every `mux` in this suite now | ||
| 8 | # writes a tile into the shared $XDG_STATE_HOME — so a file read out of | ||
| 9 | # that one would be every other block's attaches as much as this one's. | ||
| 10 | # The daemon is separate because the tile spelling contains the socket | ||
| 11 | # path, which is what the assertions grep for. | ||
| 12 | SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock" | ||
| 13 | defer_sock "$SOCK40" | ||
| 14 | WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$" | ||
| 15 | defer_rm "$WHSTATE" | ||
| 16 | WHWALL="$WHSTATE/mux/wall" | ||
| 17 | # A state home whose wall FILE is a directory: the unwritable case, which | ||
| 18 | # has to warn and let the attach happen anyway. | ||
| 19 | WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$" | ||
| 20 | defer_rm "$WHBAD" | ||
| 21 | # And a third, for the `x` block: its wall is built by two attaches and | ||
| 22 | # then eaten by `x`, so it must start empty and stay its own. | ||
| 23 | WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$" | ||
| 24 | defer_rm "$WHXSTATE" | ||
| 25 | # The convergence block: `mux TARGET` is a wall of one tile whose rect is | ||
| 26 | # the whole terminal. Three daemons and three state homes, each of them its own | ||
| 27 | # for the same reason the attach-history block's is — what these legs read | ||
| 28 | # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the | ||
| 29 | # suite has ever attached to. | ||
| 30 | SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock" | ||
| 31 | defer_sock "$SOCK45" | ||
| 32 | CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$" | ||
| 33 | defer_rm "$CVSTATE" | ||
| 34 | CVWALL="$CVSTATE/mux/wall" | ||
| 35 | # The ring-grows-the-wall leg needs TWO state homes: one for the client | ||
| 36 | # under test, and one for the setup client whose attach must NOT leave a | ||
| 37 | # tile the leg would then find already there. That absence is the case | ||
| 38 | # under test — a sibling the daemon knows and the wall does not. | ||
| 39 | SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock" | ||
| 40 | defer_sock "$SOCK46" | ||
| 41 | RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$" | ||
| 42 | defer_rm "$RGSTATE" | ||
| 43 | RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$" | ||
| 44 | defer_rm "$RGOTHER" | ||
| 45 | # The exit-semantics leg: a session ending in the focused tile. | ||
| 46 | SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" | ||
| 47 | defer_sock "$SOCK47" | ||
| 48 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" | ||
| 49 | defer_rm "$XESTATE" | ||
| 50 | |||
| 51 | # ---- the wall is attach HISTORY ----------------------------------------- | ||
| 52 | # | ||
| 53 | # The mechanical rule (wall-home-screen spec, phase 2): an attach that | ||
| 54 | # CLAIMS the grid writes its tile; one that does not, does not. Four | ||
| 55 | # witnesses, in one state home nothing else writes to: | ||
| 56 | # | ||
| 57 | # * a `mux` attach adds exactly one line, spelled `#0` — the RESOLVED | ||
| 58 | # default name, because a wall line has to be a spelling a user could | ||
| 59 | # type back, not the empty wire name the attach frame carries; | ||
| 60 | # * a second attach to the same session adds nothing (dedup, byte-exact); | ||
| 61 | # * `muxa` drives the SAME session and the file is byte-identical after | ||
| 62 | # — the 0x0 invariant, made testable rather than merely stated. The | ||
| 63 | # hash is anchored on a marker the agent actually landed, so a muxa | ||
| 64 | # that did nothing at all cannot pass this leg by doing nothing; | ||
| 65 | # * a session reached by the `Ctrl-\ c` chord earns its own tile, which | ||
| 66 | # is the "the wall grows by the truth" clause — those re-dials happen | ||
| 67 | # inside client.attach's own loop and never come back through main. | ||
| 68 | # | ||
| 69 | # Then the two failure faces: an unwritable wall file warns and does NOT | ||
| 70 | # stop the attach, and 'mux wall add' refuses a bad spelling by name | ||
| 71 | # without touching the file. | ||
| 72 | start_daemon "$SOCK40" "$OUT.wh.d" "attach-history daemon never bound" --shell /bin/sh | ||
| 73 | D37PID=$DPID | ||
| 74 | |||
| 75 | pipe_mux "$OUT.wh1" "$OUT.wh1.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 76 | pipe_send 'printf "wh1-%%s\\n" pin\n' | ||
| 77 | await_out "$OUT.wh1" "wh1-pin" "wh1-pin never reached the client" | ||
| 78 | pipe_detach | ||
| 79 | wait_grid "$SOCK40" "wh1-pin" "attach history: the default session's marker" | ||
| 80 | grep -qx -- "--sock $SOCK40#0" "$WHWALL" || { | ||
| 81 | echo "e2e FAIL: attach history: the attach wrote no tile (or not '#0'):" | ||
| 82 | cat "$WHWALL" 2>&1; exit 1; } | ||
| 83 | _wh_n=$(wc -l < "$WHWALL") | ||
| 84 | [ "$_wh_n" -eq 1 ] || { | ||
| 85 | echo "e2e FAIL: attach history: one attach wrote $_wh_n lines:" | ||
| 86 | cat "$WHWALL"; exit 1; } | ||
| 87 | |||
| 88 | pipe_mux "$OUT.wh2" "$OUT.wh2.err" env XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 89 | pipe_send 'printf "wh2-%%s\\n" pin\n' | ||
| 90 | await_out "$OUT.wh2" "wh2-pin" "wh2-pin never reached the client" | ||
| 91 | pipe_detach | ||
| 92 | wait_grid "$SOCK40" "wh2-pin" "attach history: the second attach's marker" | ||
| 93 | _wh_n=$(wc -l < "$WHWALL") | ||
| 94 | [ "$_wh_n" -eq 1 ] || { | ||
| 95 | echo "e2e FAIL: attach history: a second attach to the same session made" | ||
| 96 | echo " $_wh_n lines — dedup is not deduping:" | ||
| 97 | cat "$WHWALL"; exit 1; } | ||
| 98 | |||
| 99 | # muxa: the 0x0 invariant, as a hash. Its marker is the anchor — a muxa | ||
| 100 | # that never attached would leave the file identical too, and this leg | ||
| 101 | # would pass while proving nothing. | ||
| 102 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 103 | XDG_STATE_HOME="$WHSTATE" "$MUXA" send 'printf "whagent-%s\n" pin\n' \ | ||
| 104 | --sock "$SOCK40" > "$OUT.whb" 2> "$OUT.whb.err" | ||
| 105 | wait_grid "$SOCK40" "whagent-pin" "attach history: the agent's own marker" | ||
| 106 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 107 | echo "e2e FAIL: attach history: muxa attached at 0x0 and still wrote a tile" | ||
| 108 | echo " — 'muxa never touches the wall' is broken:" | ||
| 109 | cat "$WHWALL"; exit 1; } | ||
| 110 | |||
| 111 | # Ctrl-\ c: the chord re-dials full size, so the session it creates earns a | ||
| 112 | # tile like any other attach. The chord gets a write of its own — a chord | ||
| 113 | # ends its chunk, and a piped client reads chunks. | ||
| 114 | { sleep 1; printf '\034c'; sleep 3; printf 'printf "whc-%%s\\n" pin\n'; \ | ||
| 115 | sleep 2; printf '\034\034'; } | \ | ||
| 116 | XDG_STATE_HOME="$WHSTATE" timeout 40 "$MUX" --sock "$SOCK40" \ | ||
| 117 | > "$OUT.whc" 2> "$OUT.whc.err" | ||
| 118 | wait_grid "$SOCK40" "whc-pin" "attach history: the chord-created session's marker" 1 | ||
| 119 | grep -qx -- "--sock $SOCK40#1" "$WHWALL" || { | ||
| 120 | echo "e2e FAIL: attach history: a session visited by Ctrl-\\ c earned no tile:" | ||
| 121 | cat "$WHWALL"; cat "$OUT.whc.err"; exit 1; } | ||
| 122 | |||
| 123 | # An unwritable wall file: a DIRECTORY where the file belongs. One warning, | ||
| 124 | # and an attach that happened anyway — best effort means the attach is the | ||
| 125 | # act and the tile is only the record. | ||
| 126 | mkdir -p "$WHBAD/mux/wall" | ||
| 127 | pipe_mux "$OUT.whro" "$OUT.whro.err" env XDG_STATE_HOME="$WHBAD" timeout 40 "$MUX" --sock "$SOCK40" | ||
| 128 | pipe_send 'printf "whro-%%s\\n" pin\n' | ||
| 129 | await_out "$OUT.whro" "whro-pin" "whro-pin never reached the client" | ||
| 130 | pipe_detach | ||
| 131 | grep -q "wall not updated" "$OUT.whro.err" || { | ||
| 132 | echo "e2e FAIL: attach history: an unwritable wall file said nothing:" | ||
| 133 | cat "$OUT.whro.err"; exit 1; } | ||
| 134 | wait_grid "$SOCK40" "whro-pin" "attach history: the attach an unwritable wall did not block" | ||
| 135 | |||
| 136 | # The self-attach refusal exits BEFORE the dial, so it must exit before the | ||
| 137 | # write too: a tile is the record of an attach, and this one never happened. | ||
| 138 | # Spelled with the environment rather than a session shell because that is | ||
| 139 | # all the refusal reads — and it keeps the assertion a hash of one file | ||
| 140 | # rather than a search for a line other legs also write. | ||
| 141 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 142 | set +e | ||
| 143 | MUX_SOCK="$SOCK40" MUX_SESSION=0 XDG_STATE_HOME="$WHSTATE" \ | ||
| 144 | "$MUX" --sock "$SOCK40" > "$OUT.whself" 2>&1 | ||
| 145 | RC=$? | ||
| 146 | set -e | ||
| 147 | [ "$RC" -eq 2 ] || { | ||
| 148 | echo "e2e FAIL: attach history: the self-attach refusal exited $RC, want 2:" | ||
| 149 | cat "$OUT.whself"; exit 1; } | ||
| 150 | grep -q "this shell is inside that session" "$OUT.whself" || { | ||
| 151 | echo "e2e FAIL: attach history: the self-attach leg refused for another reason:" | ||
| 152 | cat "$OUT.whself"; exit 1; } | ||
| 153 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 154 | echo "e2e FAIL: attach history: a REFUSED self-attach still wrote a tile:" | ||
| 155 | cat "$WHWALL"; exit 1; } | ||
| 156 | |||
| 157 | # 'mux wall add': validation at ADD time, in the grammar's own words, and | ||
| 158 | # a refused spelling writes nothing at all. | ||
| 159 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 160 | set +e | ||
| 161 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall add "box#has space" > "$OUT.whadd" 2>&1 | ||
| 162 | RC=$? | ||
| 163 | set -e | ||
| 164 | [ "$RC" -ne 0 ] || { | ||
| 165 | echo "e2e FAIL: attach history: 'mux wall add' accepted a bad session name"; exit 1; } | ||
| 166 | grep -q "bad session name" "$OUT.whadd" || { | ||
| 167 | echo "e2e FAIL: attach history: a refused add never named its reason:" | ||
| 168 | cat "$OUT.whadd"; exit 1; } | ||
| 169 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 170 | echo "e2e FAIL: attach history: a refused add still edited the file:" | ||
| 171 | cat "$WHWALL"; exit 1; } | ||
| 172 | |||
| 173 | # ...and the pair that works, including `rm` of what is not there. | ||
| 174 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall add "whbox#work" > "$OUT.whadd" 2>&1 | ||
| 175 | grep -qx "whbox#work" "$WHWALL" || { | ||
| 176 | echo "e2e FAIL: attach history: 'mux wall add' added nothing:"; cat "$WHWALL"; exit 1; } | ||
| 177 | set +e | ||
| 178 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall rm "whbox#work" > "$OUT.whrm" 2>&1 | ||
| 179 | RC=$? | ||
| 180 | set -e | ||
| 181 | [ "$RC" -eq 0 ] || { | ||
| 182 | echo "e2e FAIL: attach history: 'mux wall rm' exited $RC:"; cat "$OUT.whrm"; exit 1; } | ||
| 183 | if grep -q "whbox#work" "$WHWALL"; then | ||
| 184 | echo "e2e FAIL: attach history: 'mux wall rm' left the line behind:" | ||
| 185 | cat "$WHWALL"; exit 1 | ||
| 186 | fi | ||
| 187 | set +e | ||
| 188 | XDG_STATE_HOME="$WHSTATE" "$MUX" wall rm "whbox#work" > "$OUT.whrm" 2>&1 | ||
| 189 | RC=$? | ||
| 190 | set -e | ||
| 191 | [ "$RC" -ne 0 ] || { | ||
| 192 | echo "e2e FAIL: attach history: removing an absent tile exited 0"; exit 1; } | ||
| 193 | grep -q "not on the wall" "$OUT.whrm" || { | ||
| 194 | echo "e2e FAIL: attach history: an absent rm never said so:"; cat "$OUT.whrm"; exit 1; } | ||
| 195 | ok "the wall is attach history: mux adds, muxa never does, add/rm edit it" | ||
| 196 | |||
| 197 | # ---- `x` forgets a tile, and never kills its session -------------------- | ||
| 198 | # | ||
| 199 | # The wall this leg puts up is one nothing typed by hand: two attaches | ||
| 200 | # BUILT it, which is the model's whole claim. Then `Ctrl-\ x` is asked for | ||
| 201 | # the two things it must do and the one it must not — remove the line, | ||
| 202 | # re-cut the wall, and leave the session running ("remove is detach"). | ||
| 203 | # | ||
| 204 | # Two ptyclient runs rather than one, because the file is the artifact: | ||
| 205 | # what a run did is only readable after it has exited, so the one-tile | ||
| 206 | # state between them is the second run's input. | ||
| 207 | pipe_mux "$OUT.whxa" "$OUT.whxa.err" env XDG_STATE_HOME="$WHXSTATE" timeout 40 "$MUX" --sock "$SOCK40" --session xa | ||
| 208 | pipe_send 'printf "whxa-%%s\\n" pin\n' | ||
| 209 | await_out "$OUT.whxa" "whxa-pin" "whxa-pin never reached the client" | ||
| 210 | pipe_detach | ||
| 211 | wait_grid "$SOCK40" "whxa-pin" "x forgets: session xa's marker" xa | ||
| 212 | pipe_mux "$OUT.whxb" "$OUT.whxb.err" env XDG_STATE_HOME="$WHXSTATE" timeout 40 "$MUX" --sock "$SOCK40" --session xb | ||
| 213 | pipe_send 'printf "whxb-%%s\\n" pin\n' | ||
| 214 | await_out "$OUT.whxb" "whxb-pin" "whxb-pin never reached the client" | ||
| 215 | pipe_detach | ||
| 216 | wait_grid "$SOCK40" "whxb-pin" "x forgets: session xb's marker" xb | ||
| 217 | _whx_n=$(wc -l < "$WHXSTATE/mux/wall") | ||
| 218 | [ "$_whx_n" -eq 2 ] || { | ||
| 219 | echo "e2e FAIL: x forgets: two attaches built a wall of $_whx_n tiles:" | ||
| 220 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 221 | |||
| 222 | # No operands: the wall this shows is the file, which is the point. The | ||
| 223 | # state home rides on PTYCLIENT itself and is inherited by the child — | ||
| 224 | # ptyclient execs its `--` argv as given, so an `env` wrapper in front of | ||
| 225 | # `mux` is not a command it runs, it is a program it fails to find. | ||
| 226 | # | ||
| 227 | # ONE marker expected, then `settle` — the pattern every other two-tile | ||
| 228 | # ptyclient leg here uses, and for a reason review reproduced on this | ||
| 229 | # suite: the tiles are independent threads, `expect` consumes forward, and | ||
| 230 | # whichever stripe painted second leaves the other one's marker BEHIND the | ||
| 231 | # cursor. Two consecutive expects against two pumps is a coin flip that | ||
| 232 | # spends its whole budget waiting for bytes that already went past. What | ||
| 233 | # the `Ctrl-\ x` below needs is a wall that is up and settled, which the | ||
| 234 | # single expect plus `settle` gives; the tile it forgets is the FOCUSED | ||
| 235 | # one, tile 0, whose line the file is then asserted NOT to hold. | ||
| 236 | set +e | ||
| 237 | XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 238 | --out "$OUT.whxcap" --err "$OUT.whxcap.err" -- \ | ||
| 239 | "$MUX" wall > "$OUT.whxpc" 2>&1 <<'EOF' | ||
| 240 | expect whxb-pin 20000 | ||
| 241 | settle 800 20000 | ||
| 242 | send \x1cx | ||
| 243 | settle 800 20000 | ||
| 244 | send \x1cd | ||
| 245 | waitexit 10000 | ||
| 246 | EOF | ||
| 247 | RC=$? | ||
| 248 | set -e | ||
| 249 | [ "$RC" -eq 0 ] || { | ||
| 250 | echo "e2e FAIL: x forgets: ptyclient leg exited $RC (did \\x1cx forget the tile?):" | ||
| 251 | cat "$OUT.whxpc"; exit 1; } | ||
| 252 | # The line is gone and the survivor kept its place — `Ctrl-\ x` is an | ||
| 253 | # ordered removal, not a rewrite of the wall. | ||
| 254 | _whx_n=$(wc -l < "$WHXSTATE/mux/wall") | ||
| 255 | [ "$_whx_n" -eq 1 ] || { | ||
| 256 | echo "e2e FAIL: x forgets: the wall file holds $_whx_n lines after one \\x1cx:" | ||
| 257 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 258 | grep -qx -- "--sock $SOCK40#xb" "$WHXSTATE/mux/wall" || { | ||
| 259 | echo "e2e FAIL: x forgets: the wrong line survived:" | ||
| 260 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 261 | # The survivor took the whole terminal: its marker painted a SECOND time | ||
| 262 | # after the re-cut, counted because one paint is the wall's first draw and | ||
| 263 | # says nothing about the hole closing around the survivor. | ||
| 264 | _whx_bars=$(grep -o "whxb-pin" "$OUT.whxcap" | wc -l) | ||
| 265 | [ "$_whx_bars" -ge 2 ] || { | ||
| 266 | echo "e2e FAIL: x forgets: the surviving tile's marker painted $_whx_bars time(s)," | ||
| 267 | echo " so the wall was never re-cut around the hole:" | ||
| 268 | cat "$OUT.whxpc"; exit 1; } | ||
| 269 | # ...and the SESSION is untouched: it still answers, and it still holds | ||
| 270 | # what it held. "Remove is detach" — the tile went, the session did not. | ||
| 271 | # xa was 80-wide from its pipe attach; this 40-wide wall resized it on | ||
| 272 | # hydrate, and the detach left that size behind. | ||
| 273 | XDG_STATE_HOME="$WHSTATE" "$MUXA" status --sock "$SOCK40" --session xa > "$OUT.whxst" 2>&1 | ||
| 274 | grep -q '"cols":40' "$OUT.whxst" || { | ||
| 275 | echo "e2e FAIL: x forgets: session xa stopped answering — \\x1cx killed it:" | ||
| 276 | cat "$OUT.whxst"; exit 1; } | ||
| 277 | wait_grid "$SOCK40" "whxa-pin" "x forgets: xa's grid outlived its tile" xa | ||
| 278 | |||
| 279 | # The last tile: an empty wall SAYS so rather than going blank. | ||
| 280 | set +e | ||
| 281 | XDG_STATE_HOME="$WHXSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 282 | --out "$OUT.whxcap2" --err "$OUT.whxcap2.err" -- \ | ||
| 283 | "$MUX" wall > "$OUT.whxpc2" 2>&1 <<'EOF' | ||
| 284 | expect whxb-pin 20000 | ||
| 285 | settle 800 20000 | ||
| 286 | send \x1cx | ||
| 287 | settle 800 20000 | ||
| 288 | send \x1cd | ||
| 289 | waitexit 10000 | ||
| 290 | EOF | ||
| 291 | RC=$? | ||
| 292 | set -e | ||
| 293 | [ "$RC" -eq 0 ] || { | ||
| 294 | echo "e2e FAIL: x forgets: the last-tile leg exited $RC:" | ||
| 295 | cat "$OUT.whxpc2"; exit 1; } | ||
| 296 | grep -q "nothing left to show" "$OUT.whxcap2" || { | ||
| 297 | echo "e2e FAIL: x forgets: forgetting the last tile left a blank terminal" | ||
| 298 | echo " with nothing said:"; cat "$OUT.whxpc2"; exit 1; } | ||
| 299 | [ ! -s "$WHXSTATE/mux/wall" ] || { | ||
| 300 | echo "e2e FAIL: x forgets: the last \\x1cx left lines behind:" | ||
| 301 | cat "$WHXSTATE/mux/wall"; exit 1; } | ||
| 302 | # The phantom tile, while this daemon is FULL: this leg's own four (0, 1, | ||
| 303 | # xa, xb) plus the 28 filled here are max_sessions, so a FIRST attach to | ||
| 304 | # one more name is refused — and a refusal must leave no line behind. Its | ||
| 305 | # state home is neither $WHSTATE nor $WHXSTATE, both of whose walls are | ||
| 306 | # asserted on. This is the case that made the seam move off "the dial | ||
| 307 | # succeeded" — a dial that comes up is not an attach that landed, and only | ||
| 308 | # a SWITCH's refusal ever had somewhere to undo the write from. The leg | ||
| 309 | # cannot pass vacuously: a daemon with room would accept the attach and | ||
| 310 | # the rc check below would fail loudly. | ||
| 311 | fill_sessions "$SOCK40" "$OUT.whfill" fill 4 31 | ||
| 312 | WHHASH=$(sha256sum "$WHWALL" | cut -d' ' -f1) | ||
| 313 | set +e | ||
| 314 | { sleep 1; printf '\034\034'; } | XDG_STATE_HOME="$WHSTATE" timeout 40 \ | ||
| 315 | "$MUX" --sock "$SOCK40" --session phantom > "$OUT.whph" 2>&1 | ||
| 316 | RC=$? | ||
| 317 | set -e | ||
| 318 | [ "$RC" -ne 0 ] || { | ||
| 319 | echo "e2e FAIL: x forgets: a 33rd session was created on a full daemon," | ||
| 320 | echo " so the phantom-tile leg proved nothing:"; cat "$OUT.whph"; exit 1; } | ||
| 321 | [ "$WHHASH" = "$(sha256sum "$WHWALL" | cut -d' ' -f1)" ] || { | ||
| 322 | echo "e2e FAIL: x forgets: a REFUSED first attach recorded a phantom tile:" | ||
| 323 | cat "$WHWALL"; exit 1; } | ||
| 324 | assert_stopped "$SOCK40" "$D37PID" "x forgets" "$OUT.whstop" | ||
| 325 | D37PID="" | ||
| 326 | ok "\\x1cx forgets a tile and leaves its session running; a refusal records nothing" | ||
| 327 | |||
| 328 | # ---- `mux TARGET` is a wall of one tile --------------------------------- | ||
| 329 | # | ||
| 330 | # The convergence's headline: `mux` dials and attaches exactly as it always | ||
| 331 | # did, and what it runs is the WALL — one tile whose rect is the whole | ||
| 332 | # terminal, no label bar, byte-identical to a plain client. `Ctrl-\ w` | ||
| 333 | # folds the SAVED wall into this one: it hydrates the file's tiles beside | ||
| 334 | # the tile this run is standing on, and what appears is THE wall — this | ||
| 335 | # run's tile plus the ones the saved file already holds. | ||
| 336 | # | ||
| 337 | # Four claims, and each needs a different kind of witness: | ||
| 338 | # | ||
| 339 | # * the wall `Ctrl-\ w` folds in is the SAVED one. A tile written by an | ||
| 340 | # EARLIER `mux` (the `#side` attach below, into this leg's own state | ||
| 341 | # home) paints a stripe here, in a process that was never told about | ||
| 342 | # it on its command line. A wall that only ever showed this run's own | ||
| 343 | # tile would satisfy every other assertion in this leg. | ||
| 344 | # * the focus stays on the entry tile, and that costs NOTHING at the | ||
| 345 | # daemon. The attach counter is sampled across the fold and the | ||
| 346 | # injection and moves only for the two real attaches (the entry tile | ||
| 347 | # and the saved tile the fold hydrated): focus is client-local and | ||
| 348 | # sends nothing on the wire. | ||
| 349 | # * `Ctrl-\ d` leaves MUX. There is no child and no second screen; the | ||
| 350 | # one-tile wall is the program, and the detach chord is the way out. | ||
| 351 | # * ...and the attach that opened all this earned exactly one line in the | ||
| 352 | # file, spelled `#0`, beside the `#side` that was already there. | ||
| 353 | # | ||
| 354 | # A state home of its own: the wall `Ctrl-\ w` folds in is the saved one, | ||
| 355 | # and the shared $XDG_STATE_HOME is every socket the suite has ever | ||
| 356 | # attached to. | ||
| 357 | start_daemon "$SOCK45" "$OUT.cv.d" "convergence daemon never bound" --shell /bin/sh | ||
| 358 | D45PID=$DPID | ||
| 359 | |||
| 360 | # The tile that is already on the wall before this leg's client starts. A | ||
| 361 | # piped `mux`, so its attach writes the line and then goes: what the leg | ||
| 362 | # asserts on is the FILE it left behind, not a process still holding it. | ||
| 363 | pipe_mux "$OUT.cvside" "$OUT.cvside.err" env XDG_STATE_HOME="$CVSTATE" timeout 40 "$MUX" --sock "$SOCK45" --session side | ||
| 364 | pipe_send 'printf "cvside-%%s\\n" pin\n' | ||
| 365 | await_out "$OUT.cvside" "cvside-pin" "cvside-pin never reached the client" | ||
| 366 | pipe_detach | ||
| 367 | wait_grid "$SOCK45" "cvside-pin" "convergence: the saved tile's marker" side | ||
| 368 | grep -qx -- "--sock $SOCK45#side" "$CVWALL" || { | ||
| 369 | echo "e2e FAIL: convergence: the setup attach wrote no tile to fold in:" | ||
| 370 | cat "$CVWALL" 2>&1; exit 1; } | ||
| 371 | |||
| 372 | # The whole run's budget, and every one of the three is named: the entry | ||
| 373 | # tile's attach, the saved tile's when the fold hydrates it, and the | ||
| 374 | # `muxa send` below, which is an attach like any other. The fold itself | ||
| 375 | # is a real attach (it dials the saved tile's session); the focus move and | ||
| 376 | # the leave add nothing, and a cumulative counter is what says so — a | ||
| 377 | # gauge would let a dial that opened and closed between two samples hide | ||
| 378 | # inside it. | ||
| 379 | CVATT_BEFORE=$(attaches_now "$SOCK45") | ||
| 380 | ( sleep 6; "$MUXA" send 'printf "cvret-%s\n" pin\n' \ | ||
| 381 | --sock "$SOCK45" --session 0 > "$OUT.cvinj" 2>&1 ) & | ||
| 382 | CVINJPID=$! | ||
| 383 | defer_kill "$CVINJPID" | ||
| 384 | set +e | ||
| 385 | # tall: Ctrl-\ w folds in a saved tile, making two at 100x30 (100 >= 60); | ||
| 386 | # 70x36 stays stacked (70 < 72) and fits the bar label the assertion greps | ||
| 387 | XDG_STATE_HOME="$CVSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \ | ||
| 388 | --out "$OUT.cvcap" --err "$OUT.cvcap.err" \ | ||
| 389 | -- "$MUX" --sock "$SOCK45" > "$OUT.cvpc" 2>&1 <<'EOF' | ||
| 390 | expect \x1b[?1049h 15000 | ||
| 391 | settle 400 15000 | ||
| 392 | send printf 'cvhome-%s\\n' pin\n | ||
| 393 | expect cvhome-pin 15000 | ||
| 394 | settle 400 15000 | ||
| 395 | send \x1cw | ||
| 396 | expect cvside-pin 20000 | ||
| 397 | settle 900 20000 | ||
| 398 | expect cvret-pin 20000 | ||
| 399 | settle 500 15000 | ||
| 400 | send \x1cd | ||
| 401 | waitexit 10000 | ||
| 402 | EOF | ||
| 403 | RC=$? | ||
| 404 | set -e | ||
| 405 | wait "$CVINJPID" 2>/dev/null || true | ||
| 406 | [ "$RC" -eq 0 ] || { | ||
| 407 | echo "e2e FAIL: convergence: ptyclient leg exited $RC (did \\x1cw fold, did \\x1cd leave?):" | ||
| 408 | cat "$OUT.cvpc"; cat "$OUT.cvinj" 2>/dev/null; exit 1; } | ||
| 409 | # The saved tile really painted a STRIPE, which only a wall draws — a bar | ||
| 410 | # with a spelling and a state word in it. A bare marker grep would be | ||
| 411 | # satisfied by the setup attach's own capture bleeding through. | ||
| 412 | grep -q -- "--sock $SOCK45#side \[up\]" "$OUT.cvcap" || { | ||
| 413 | echo "e2e FAIL: convergence: the saved wall's tile never became a stripe:" | ||
| 414 | cat "$OUT.cvpc"; exit 1; } | ||
| 415 | grep -q -- "--sock $SOCK45#0 \[up\]" "$OUT.cvcap" || { | ||
| 416 | echo "e2e FAIL: convergence: the attached tile never became a stripe:" | ||
| 417 | cat "$OUT.cvpc"; exit 1; } | ||
| 418 | # The fold was a real attach (the saved tile dialled) and the injection was | ||
| 419 | # another; the focus move and the leave added nothing. THREE is the whole | ||
| 420 | # budget — entry tile, hydrated saved tile, injector — and a focus that | ||
| 421 | # re-dialled would make it four. | ||
| 422 | CVATT=$(attaches_now "$SOCK45") | ||
| 423 | assert_attach_delta "$CVATT_BEFORE" "$CVATT" 3 "convergence" | ||
| 424 | # The file: one line for the attach this run made, beside the one it found. | ||
| 425 | _cv_n=$(wc -l < "$CVWALL") | ||
| 426 | [ "$_cv_n" -eq 2 ] || { | ||
| 427 | echo "e2e FAIL: convergence: the wall file holds $_cv_n lines, want 2:" | ||
| 428 | cat "$CVWALL"; exit 1; } | ||
| 429 | grep -qx -- "--sock $SOCK45#0" "$CVWALL" || { | ||
| 430 | echo "e2e FAIL: convergence: the attach earned no tile of its own:" | ||
| 431 | cat "$CVWALL"; exit 1; } | ||
| 432 | assert_stopped "$SOCK45" "$D45PID" "convergence" "$OUT.cvstop" | ||
| 433 | D45PID="" | ||
| 434 | ok "mux is the wall: Ctrl-\\ w folds in the saved tiles, the focus stays put, Ctrl-\\ d leaves" | ||
| 435 | |||
| 436 | # ---- the ring GROWS the wall, and only when it has to -------------------- | ||
| 437 | # | ||
| 438 | # The spec's `n`/`p` rule, which phase 3c is where it lands: those chords | ||
| 439 | # move the focus around the DAEMON'S session ring, not around the wall's | ||
| 440 | # tiles. A sibling that already has a tile is an instant focus move; one | ||
| 441 | # that does not GETS one, because visiting it is an attach and attach adds. | ||
| 442 | # | ||
| 443 | # The leg is built so the two halves are the SAME chord pressed twice, and | ||
| 444 | # the difference between them is the whole assertion: | ||
| 445 | # | ||
| 446 | # * `\x1cn` from session 0 lands on `two`, which this run has never seen | ||
| 447 | # — its tile was written into a DIFFERENT state home by the setup | ||
| 448 | # client, so the daemon knows the session and this wall does not. One | ||
| 449 | # dial, one attach, one new line in the file, and the marker typed | ||
| 450 | # afterwards lands in `two`'s shell. | ||
| 451 | # * `\x1cn` again wraps back to 0, which now HAS a tile. Nothing is | ||
| 452 | # dialled: the counter must not move across that step, and the marker | ||
| 453 | # lands back in session 0. | ||
| 454 | # | ||
| 455 | # The attach budget for the run is therefore exactly 2 (the entry tile and | ||
| 456 | # the tile the first `n` grew), and the second `n` is the one that must be | ||
| 457 | # free. Sampled around the whole ptyclient run rather than around each | ||
| 458 | # step, because a step that dialled and closed between two samples would | ||
| 459 | # hide inside a gauge — the counter is cumulative and cannot. | ||
| 460 | start_daemon "$SOCK46" "$OUT.rg.d" "ring-grow daemon never bound" --shell /bin/sh | ||
| 461 | D46PID=$DPID | ||
| 462 | |||
| 463 | # `two` exists on the DAEMON but not in this leg's wall: a separate state | ||
| 464 | # home is what makes the setup client's own tile invisible here, so the | ||
| 465 | # first `n` meets a sibling with no tile — which is the case under test. | ||
| 466 | pipe_mux "$OUT.rgtwo" "$OUT.rgtwo.err" env XDG_STATE_HOME="$RGOTHER" timeout 40 "$MUX" --sock "$SOCK46" --session two | ||
| 467 | pipe_send 'printf "rgtwo-%%s\\n" pin\n' | ||
| 468 | await_out "$OUT.rgtwo" "rgtwo-pin" "rgtwo-pin never reached the client" | ||
| 469 | pipe_detach | ||
| 470 | wait_grid "$SOCK46" "rgtwo-pin" "ring grow: the untiled sibling's marker" two | ||
| 471 | grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" 2>/dev/null && { | ||
| 472 | echo "e2e FAIL: ring grow: the sibling already had a tile, so the ADD is vacuous:" | ||
| 473 | cat "$RGSTATE/mux/wall"; exit 1; } | ||
| 474 | |||
| 475 | RGATT_BEFORE=$(attaches_now "$SOCK46") | ||
| 476 | set +e | ||
| 477 | # tall: Ctrl-\ n grows the wall to two tiles at 100x30 (100 >= 60) | ||
| 478 | XDG_STATE_HOME="$RGSTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 479 | --out "$OUT.rgcap" --err "$OUT.rgcap.err" \ | ||
| 480 | -- "$MUX" --sock "$SOCK46" > "$OUT.rgpc" 2>&1 <<'EOF' | ||
| 481 | expect \x1b[?1049h 15000 | ||
| 482 | settle 400 15000 | ||
| 483 | send printf 'rgzero-%s\\n' pin\n | ||
| 484 | expect rgzero-pin 15000 | ||
| 485 | settle 400 15000 | ||
| 486 | send \x1cn | ||
| 487 | expect rgtwo-pin 20000 | ||
| 488 | settle 500 15000 | ||
| 489 | send printf 'rghere-%s\\n' pin\n | ||
| 490 | expect rghere-pin 15000 | ||
| 491 | settle 400 15000 | ||
| 492 | send \x1cn | ||
| 493 | expect rgzero-pin 20000 | ||
| 494 | settle 500 15000 | ||
| 495 | send printf 'rgback-%s\\n' pin\n | ||
| 496 | expect rgback-pin 15000 | ||
| 497 | settle 400 15000 | ||
| 498 | send \x1c\x1c | ||
| 499 | waitexit 10000 | ||
| 500 | EOF | ||
| 501 | RC=$? | ||
| 502 | set -e | ||
| 503 | RGATT=$(attaches_now "$SOCK46") | ||
| 504 | [ "$RC" -eq 0 ] || { | ||
| 505 | echo "e2e FAIL: ring grow: ptyclient leg exited $RC (did \\x1cn step the ring?):" | ||
| 506 | cat "$OUT.rgpc"; exit 1; } | ||
| 507 | # TWO attaches for the whole run: the entry tile's, and the one the first | ||
| 508 | # `n` grew. A `n` that re-dialled the tile it already had would make it | ||
| 509 | # three, whichever way it closed the old one first. | ||
| 510 | assert_attach_delta "$RGATT_BEFORE" "$RGATT" 2 "ring grow" | ||
| 511 | # The wall grew by the truth: the sibling the ring visited earned a line. | ||
| 512 | grep -qx -- "--sock $SOCK46#two" "$RGSTATE/mux/wall" || { | ||
| 513 | echo "e2e FAIL: ring grow: the visited sibling earned no tile:" | ||
| 514 | cat "$RGSTATE/mux/wall" 2>&1; exit 1; } | ||
| 515 | _rg_n=$(wc -l < "$RGSTATE/mux/wall") | ||
| 516 | [ "$_rg_n" -eq 2 ] || { | ||
| 517 | echo "e2e FAIL: ring grow: the wall file holds $_rg_n lines, want 2:" | ||
| 518 | cat "$RGSTATE/mux/wall"; exit 1; } | ||
| 519 | # Where the markers landed says the focus really MOVED rather than the wall | ||
| 520 | # having gone on typing at one connection. Each session holds its own and | ||
| 521 | # not its neighbour's. | ||
| 522 | timeout 20 "$MUXA" capture --sock "$SOCK46" --session two > "$OUT.rgcapt" 2>&1 | ||
| 523 | grep -q "rghere-pin" "$OUT.rgcapt" || { | ||
| 524 | echo "e2e FAIL: ring grow: the marker typed after the first n missed session two:" | ||
| 525 | cat "$OUT.rgcapt"; exit 1; } | ||
| 526 | grep -q "rgback-pin" "$OUT.rgcapt" && { | ||
| 527 | echo "e2e FAIL: ring grow: the marker typed after the wrap landed in two:" | ||
| 528 | cat "$OUT.rgcapt"; exit 1; } | ||
| 529 | timeout 20 "$MUXA" capture --sock "$SOCK46" --session 0 > "$OUT.rgcap0" 2>&1 | ||
| 530 | grep -q "rgback-pin" "$OUT.rgcap0" || { | ||
| 531 | echo "e2e FAIL: ring grow: the wrap did not come back to session 0:" | ||
| 532 | cat "$OUT.rgcap0"; exit 1; } | ||
| 533 | assert_stopped "$SOCK46" "$D46PID" "ring grow" "$OUT.rgstop" | ||
| 534 | D46PID="" | ||
| 535 | ok "Ctrl-\\ n grows the wall for a sibling with no tile, and is free for one that has" | ||
| 536 | |||
| 537 | # ---- a session that ends in the focused tile --------------------------- | ||
| 538 | # | ||
| 539 | # The exit rule the convergence had to choose, stated in decisions.md and | ||
| 540 | # pinned here: a session that ends in the focused tile ENDS MUX when it was | ||
| 541 | # the wall's only tile, and LEAVES THE FOCUS TO A NEIGHBOUR when it was not. | ||
| 542 | # | ||
| 543 | # Both halves in one block, because either alone is satisfied by a bug: | ||
| 544 | # | ||
| 545 | # * one tile. `exit 7` in a piped `mux` — the shape every script in the | ||
| 546 | # world uses — and the code comes back 7. Graded against the mutation | ||
| 547 | # (`endAction` dropping to the wall whatever the tile count): `mux` | ||
| 548 | # comes back 0, because the wall it dropped to reads this script's | ||
| 549 | # closed stdin as a wall nobody is left to steer. Zero is not seven and | ||
| 550 | # the leg says so; 124 is the other shape the same bug can take. | ||
| 551 | # * two tiles. `\x1cc` adds one and focuses it, the shell in it exits, and | ||
| 552 | # the tile vanishes — a dead stripe for a cleanly exited shell is noise | ||
| 553 | # — leaving the survivor the whole screen. Then `\x1cd` leaves — exit | ||
| 554 | # 0, not 7 and not the shell's anything, because leaving is the user's | ||
| 555 | # act and not the session's. | ||
| 556 | # * two tiles and a PIPED client whose stdin has already gone. The case | ||
| 557 | # review reproduced as a hang, and the reason the rule's second half is | ||
| 558 | # stated in `endAction` rather than at the read that notices EOF. | ||
| 559 | start_daemon "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound" --shell /bin/sh | ||
| 560 | D47PID=$DPID | ||
| 561 | |||
| 562 | # A wall it dropped to instead would read as 124: it never leaves, because | ||
| 563 | # nothing closes its stdin under it. | ||
| 564 | pipe_mux "$OUT.xe1" "$OUT.xe1.err" env XDG_STATE_HOME="$XESTATE" timeout 40 \ | ||
| 565 | "$MUX" --sock "$SOCK47" | ||
| 566 | pipe_send 'exit 7\n' | ||
| 567 | pipe_waitexit "exit semantics: the only tile's shell exited 7, and mux" 7 | ||
| 568 | |||
| 569 | set +e | ||
| 570 | # tall: Ctrl-\ c creates a second tile at 100x30 (100 >= 60) | ||
| 571 | XDG_STATE_HOME="$XESTATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 \ | ||
| 572 | --out "$OUT.xecap" --err "$OUT.xecap.err" \ | ||
| 573 | -- "$MUX" --sock "$SOCK47" > "$OUT.xepc" 2>&1 <<'EOF' | ||
| 574 | expect \x1b[?1049h 15000 | ||
| 575 | settle 400 15000 | ||
| 576 | send printf 'xehome-%s\\n' pin\n | ||
| 577 | expect xehome-pin 15000 | ||
| 578 | settle 400 15000 | ||
| 579 | send \x1cc | ||
| 580 | expect \x1b[2J 15000 | ||
| 581 | settle 600 15000 | ||
| 582 | send exit 3\n | ||
| 583 | expect xehome-pin 20000 | ||
| 584 | settle 800 20000 | ||
| 585 | send \x1cd | ||
| 586 | waitexit 10000 | ||
| 587 | EOF | ||
| 588 | RC=$? | ||
| 589 | set -e | ||
| 590 | [ "$RC" -eq 0 ] || { | ||
| 591 | echo "e2e FAIL: exit semantics: the multi-tile leg exited $RC (did the focus" | ||
| 592 | echo " move to the neighbour, and did \x1cd leave it?):" | ||
| 593 | cat "$OUT.xepc"; exit 1; } | ||
| 594 | # The exited tile vanished — a dead stripe for a cleanly exited shell is | ||
| 595 | # noise — so there is no `[exited]` bar to read, and the survivor took the | ||
| 596 | # whole screen: a one-tile wall draws no label bar. The marker it repainted | ||
| 597 | # is what `expect xehome-pin` above caught; the grid check below is the | ||
| 598 | # same fact from the daemon's side. | ||
| 599 | # The session that did NOT end is still there: a shell exiting in the | ||
| 600 | # focused tile ends that session and nothing else. | ||
| 601 | wait_grid "$SOCK47" "xehome-pin" "exit semantics: session 0 outlived its neighbour" | ||
| 602 | |||
| 603 | # The third case, and the one review REPRODUCED as a hang: two tiles, but | ||
| 604 | # a PIPED client whose stdin has already closed when the focused session | ||
| 605 | # ends. Moving the focus to a neighbour is not an answer there — a wall | ||
| 606 | # nobody is left to steer has no keyboard, so mux waited in poll forever | ||
| 607 | # with the exit code in hand (RC=124, measured). The rule is the same rule | ||
| 608 | # with its second half stated: an ending nobody can leave is mux's ending. | ||
| 609 | # | ||
| 610 | # The gap is built rather than raced: the shell reads `sleep 4` and | ||
| 611 | # `exit 3` from a stdin that closes IMMEDIATELY after, so the pipe is long | ||
| 612 | # gone by the time the session ends four seconds later. `timeout` is what | ||
| 613 | # turns a regression into a failure instead of a wedged suite. | ||
| 614 | XETIME_START=$(date +%s) | ||
| 615 | set +e | ||
| 616 | { sleep 1; printf '\034c'; sleep 3; printf 'sleep 4\n'; printf 'exit 3\n'; } | \ | ||
| 617 | XDG_STATE_HOME="$XESTATE" timeout 40 "$MUX" --sock "$SOCK47" \ | ||
| 618 | > "$OUT.xe2" 2> "$OUT.xe2.err" | ||
| 619 | RC=$? | ||
| 620 | set -e | ||
| 621 | XETIME=$(( $(date +%s) - XETIME_START )) | ||
| 622 | [ "$RC" -eq 3 ] || { | ||
| 623 | echo "e2e FAIL: exit semantics: a piped two-tile client whose session ended" | ||
| 624 | echo " exited $RC after ${XETIME}s, want 3 — 124 is the wall it" | ||
| 625 | echo " left to a neighbour with nobody left to steer it:" | ||
| 626 | cat "$OUT.xe2.err"; exit 1; } | ||
| 627 | # Promptly, not eventually: the code is right in both readings, but a | ||
| 628 | # client that only exits because the timeout reaped it is the bug wearing | ||
| 629 | # the right answer. The session ends ~8s in; 20 is generous and still far | ||
| 630 | # under the 40s timeout that would otherwise be the only bound. | ||
| 631 | [ "$XETIME" -lt 20 ] || { | ||
| 632 | echo "e2e FAIL: exit semantics: the piped client took ${XETIME}s to notice its" | ||
| 633 | echo " own session had ended — it exited, but it did not notice"; exit 1; } | ||
| 634 | assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop" | ||
| 635 | D47PID="" | ||
| 636 | ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour" | ||