a73x

c5bf4186

test: xversion — the two verbs an old daemon answers with silence

a73x   2026-08-28 19:53

Commit message
test: xversion — the two verbs an old daemon answers with silence

Two legs, both new-client-against-old-daemon, both asserting what the user
actually meets. `Ctrl-\ x` on a daemon with no `end_req` arm banners
`[daemon too old to end a session]` within its deadline and the session
survives — witnessed by a marker the shell expands reaching the old
daemon's grid AFTER the chord, because the old ref predates M18 and its
`stats` has no session count to compare (leg 6 pins that absence). And a
daemon that never learned to answer `sessions_req` on an observer
connection reads `[unreachable]` on the wall, with the new daemon's count
in the same listing as the control.

Both fail under the sanctioned falsification run (XVER_OLD_BIN at the new
prefix): the banner never arrives, and the "old" daemon answers the list.

Each pty leg gets its own XDG_STATE_HOME. `mux` records the daemon it
attached to and opens the wall of everything in the file, so the shared
dir would have put leg 7's daemon on leg 8's screen.

12/12 green against ../mux-xver-old at v0.0.1-5.

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

test/xversion.sh
Old New
@@ -82,9 +82,10 @@ done
82 # path is capped near 108 bytes by the kernel, not by anything this script 82 # path is capped near 108 bytes by the kernel, not by anything this script
83 # can see fail gracefully. 83 # can see fail gracefully.
84 TMP="${TMPDIR:-/tmp}/mux-xver-$$" 84 TMP="${TMPDIR:-/tmp}/mux-xver-$$"
85 # Every attach records a wall tile in $XDG_STATE_HOME/mux/wall; without a 85 # Every named attach records a DAEMON in $XDG_STATE_HOME/mux/hosts; without
86 # hermetic home this suite's throwaway sockets pile up in the developer's 86 # a hermetic home this suite's throwaway sockets pile up on the developer's
87 # real wall file and muxweb tries to open them all. 87 # own wall, one dead stripe each. Legs that take a terminal narrow it
88 # further, one dir per leg — see `side_channel_session`.
88 XDG_STATE_HOME="$TMP/state"; XDG_CONFIG_HOME="$TMP/cfg"; XDG_CACHE_HOME="$TMP/cache" 89 XDG_STATE_HOME="$TMP/state"; XDG_CONFIG_HOME="$TMP/cfg"; XDG_CACHE_HOME="$TMP/cache"
89 export XDG_STATE_HOME XDG_CONFIG_HOME XDG_CACHE_HOME 90 export XDG_STATE_HOME XDG_CONFIG_HOME XDG_CACHE_HOME
90 RUN_OLD="$TMP/o" 91 RUN_OLD="$TMP/o"
@@ -418,6 +419,13 @@ EOF
418 # survive" — a fixture failure attributed to the subject. The outer 419 # survive" — a fixture failure attributed to the subject. The outer
419 # timeout is a backstop and has to sit outside the primary deadlines to 420 # timeout is a backstop and has to sit outside the primary deadlines to
420 # stay one. 421 # stay one.
422 # A state dir per leg. `mux` records the daemon it attached to in
423 # $XDG_STATE_HOME/mux/hosts and opens the wall of everything in there,
424 # so a shared dir would put the PREVIOUS leg's daemon on this leg's
425 # wall — every needle below would then be asserted against a screen cut
426 # into stripes by a host this leg never mentioned.
427 mkdir -p "$TMP/st-$_sc_tag"
428 XDG_STATE_HOME="$TMP/st-$_sc_tag" \
421 timeout 90 "$NEW_BIN/ptyclient" --cols 80 --rows 24 \ 429 timeout 90 "$NEW_BIN/ptyclient" --cols 80 --rows 24 \
422 --out "$TMP/sc-$_sc_tag" --err "$TMP/sc-$_sc_tag.err" \ 430 --out "$TMP/sc-$_sc_tag" --err "$TMP/sc-$_sc_tag.err" \
423 -- "$_sc_cli" "$@" > "$TMP/sc-$_sc_tag.log" 2>&1 <<SCRIPT 431 -- "$_sc_cli" "$@" > "$TMP/sc-$_sc_tag.log" 2>&1 <<SCRIPT
@@ -545,6 +553,80 @@ xver_side_channel() {
545 xver_side_channel socket --sock "$SOCK_NEW" 553 xver_side_channel socket --sock "$SOCK_NEW"
546 xver_side_channel quic "quic://127.0.0.1:$PORT_NEW" --key "$KEY" --quic-idle-ms 15000 554 xver_side_channel quic "quic://127.0.0.1:$PORT_NEW" --key "$KEY" --quic-idle-ms 15000
547 555
556 # ---- leg 9: `Ctrl-\ x` against a daemon with no end_req arm ------------
557 # This branch made `Ctrl-\ x` END the focused session (end_req 0x11). An old
558 # daemon's dispatch ends in `else => {}`, so the request is not refused, it
559 # is never heard — and an intent with no expiry would stay armed for the
560 # rest of the session, making every later chord a silent no-op. The client
561 # arms a deadline (client.PendingSwitch.wait_ms, 2s) and banners
562 # `[daemon too old to end a session]` when it lapses. That banner is the
563 # whole user-facing compat story for this verb, so it is what is asserted.
564 #
565 # On a pty, because the banner is painted into the rail on the alternate
566 # screen and never written to stderr — over a pipe there is no rail and this
567 # leg would assert nothing. Its own XDG_STATE_HOME for the reason above
568 # `side_channel_session`.
569 #
570 # The session must SURVIVE, and the witness is the shell's own work rather
571 # than a session count: the old ref predates M18, so its `muxd stats` has no
572 # `sessions=` field at all (leg 6 pins exactly that absence). A marker that
573 # the shell EXPANDED and that reaches the old daemon's grid AFTER the x is
574 # the stronger fact anyway — it says the pty is still there and still
575 # running a shell, which is what "the session did not end" means.
576 mkdir -p "$TMP/st-endold"
577 XDG_STATE_HOME="$TMP/st-endold" \
578 timeout 60 "$NEW_BIN/ptyclient" --cols 80 --rows 24 \
579 --out "$TMP/endold" --err "$TMP/endold.err" \
580 -- "$NEW_BIN/mux" --sock "$SOCK_OLD" > "$TMP/endold.log" 2>&1 <<'SCRIPT'
581 expect \x1b[?1049h 15000
582 settle 400 15000
583 send \x1cx
584 expect [daemon too old to end a session] 3000
585 send printf "xv9-%s\n" pin\n
586 expect xv9-pin 15000
587 settle 400 15000
588 send \x1c\x1c
589 waitexit 10000
590 SCRIPT
591 RC=$?
592 if [ "$RC" -ne 0 ]; then
593 fail "x -> old daemon: ptyclient exited $RC [$(tr -d '\n' < "$TMP/endold.log")]"
594 elif ! grep -qaF -- "[daemon too old to end a session]" "$TMP/endold"; then
595 fail "x -> old daemon: no banner — the chord went out and nothing said so"
596 elif ! "$OLD_BIN/muxd" dump --sock "$SOCK_OLD" | grep -q 'xv9-pin'; then
597 fail "x -> old daemon: the session did not survive the unanswered end_req"
598 else
599 pass "x against a daemon with no end_req arm banners and leaves the session running"
600 fi
601
602 # ---- leg 10: an old daemon has no session list, so the wall stripes it --
603 # The other half of the same silence, and the one a user meets first.
604 # Listing a daemon's live sessions on a bare OBSERVER connection is new
605 # here — `sessions_req` existed, but only on the attached client's path — so
606 # an old daemon answers the wall's poll with nothing and the wall paints it
607 # `[unreachable]` forever, while `mux --sock` still attaches to it happily.
608 # That is a real cost of taking this version and it is documented
609 # (decisions.md, 2026-08-28); pinned here so the sentence cannot drift.
610 #
611 # Control and subject in one leg, for leg 7's reason: if the NEW daemon does
612 # not list either, then `mux hosts` is broken rather than the old daemon
613 # being old, and a bare "[unreachable] appeared" would be green on a rig
614 # asserting nothing. `mux hosts` needs no terminal — it dials, prints and
615 # exits — so this costs no fixture.
616 mkdir -p "$TMP/st-hosts/mux"
617 printf -- '--sock %s\n--sock %s\n' "$SOCK_NEW" "$SOCK_OLD" > "$TMP/st-hosts/mux/hosts"
618 XDG_STATE_HOME="$TMP/st-hosts" timeout 30 "$NEW_BIN/mux" hosts > "$TMP/hosts.out" 2>&1
619 RC=$?
620 if [ "$RC" -ne 0 ]; then
621 fail "mux hosts exited $RC [$(tr -d '\n' < "$TMP/hosts.out")]"
622 elif ! grep -q -- "--sock $SOCK_NEW [0-9]" "$TMP/hosts.out"; then
623 fail "mux hosts did not count the NEW daemon's sessions — the old one below stripes for no reason [$(cat "$TMP/hosts.out")]"
624 elif ! grep -qF -- "--sock $SOCK_OLD [unreachable]" "$TMP/hosts.out"; then
625 fail "an old daemon answered the wall's session list — decisions.md says it cannot [$(cat "$TMP/hosts.out")]"
626 else
627 pass "a daemon with no observer sessions_req reads as [unreachable] on the wall, as documented"
628 fi
629
548 # ---- teardown, by the sanctioned verb, then OBSERVED ------------------ 630 # ---- teardown, by the sanctioned verb, then OBSERVED ------------------
549 "$NEW_BIN/muxd" stop --sock "$SOCK_NEW" >/dev/null 2>&1 631 "$NEW_BIN/muxd" stop --sock "$SOCK_NEW" >/dev/null 2>&1
550 "$OLD_BIN/muxd" stop --sock "$SOCK_OLD" >/dev/null 2>&1 632 "$OLD_BIN/muxd" stop --sock "$SOCK_OLD" >/dev/null 2>&1
@@ -568,8 +650,8 @@ fi
568 # silently stops running is the failure mode no assertion inside it can 650 # silently stops running is the failure mode no assertion inside it can
569 # catch. Skips count — a skipped leg RAN and decided not to assert. 651 # catch. Skips count — a skipped leg RAN and decided not to assert.
570 TOTAL=$((PASSES + FAILS + SKIPS)) 652 TOTAL=$((PASSES + FAILS + SKIPS))
571 if [ "$TOTAL" -ne 10 ]; then 653 if [ "$TOTAL" -ne 12 ]; then
572 echo "xver FAIL: $TOTAL legs reported, want 10 — one did not run" 654 echo "xver FAIL: $TOTAL legs reported, want 12 — one did not run"
573 FAILS=$((FAILS + 1)) 655 FAILS=$((FAILS + 1))
574 fi 656 fi
575 657