c5bf4186
test: xversion — the two verbs an old daemon answers with silence
a73x 2026-08-28 19:53
Commit message
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 | ||