a73x

ec5c8b51

test: 09_wallhist moves to a file of its own

a73x   2026-08-26 19:07

Commit message
test: 09_wallhist moves to a file of its own

the wall as attach history, and `mux TARGET` as a wall of one — 5 scenarios, 636 lines. The legs move unedited; the runner
sources the file where they used to stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

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"