5a2ed83f
test: the restored wall creates its local lines and refuses its remote one
a73x 2026-08-25 19:32
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -380,6 +380,22 @@ SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock" | |||
| 380 | SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" | 380 | SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" |
| 381 | SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock" | 381 | SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock" |
| 382 | SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock" | 382 | SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock" |
| 383 | # The hydrated-create leg. One daemon, and it serves QUIC as well as its | ||
| 384 | # socket: the leg's whole point is the LOCAL/remote split, so both halves | ||
| 385 | # have to be the same run of the same daemon or the comparison is between | ||
| 386 | # two rigs rather than between two kinds of line. Short socket name on | ||
| 387 | # purpose — the assertion reads a label bar back off a 70-column render. | ||
| 388 | SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock" | ||
| 389 | # Base 6000, below the 5000-spaced band the other QUIC ports share: every | ||
| 390 | # base from 11000 up is already taken, and a DUPLICATED base passes only | ||
| 391 | # for as long as the other leg's daemon happens to be dead by the time | ||
| 392 | # this one binds. A collision reads as this daemon's failure, not the | ||
| 393 | # suite's, which is why the base has to be the leg's own. | ||
| 394 | HYPORT=$(( 6000 + ($$ % 4000) )) | ||
| 395 | HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$" | ||
| 396 | HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$" | ||
| 397 | HYWALL="$HYSTATE/mux/wall" | ||
| 398 | D65PID="" | ||
| 383 | D54PID="" | 399 | D54PID="" |
| 384 | D55PID="" | 400 | D55PID="" |
| 385 | D56PID="" | 401 | D56PID="" |
| @@ -1335,6 +1351,7 @@ cleanup() { | |||
| 1335 | [ -n "${D62PID:-}" ] && kill "$D62PID" 2>/dev/null || true | 1351 | [ -n "${D62PID:-}" ] && kill "$D62PID" 2>/dev/null || true |
| 1336 | [ -n "${D63PID:-}" ] && kill "$D63PID" 2>/dev/null || true | 1352 | [ -n "${D63PID:-}" ] && kill "$D63PID" 2>/dev/null || true |
| 1337 | [ -n "${D64PID:-}" ] && kill "$D64PID" 2>/dev/null || true | 1353 | [ -n "${D64PID:-}" ] && kill "$D64PID" 2>/dev/null || true |
| 1354 | [ -n "${D65PID:-}" ] && kill "$D65PID" 2>/dev/null || true | ||
| 1338 | # The ssh-agents the forwarding legs start. Not mux processes and so not | 1355 | # The ssh-agents the forwarding legs start. Not mux processes and so not |
| 1339 | # the leak sweep's business, but they are daemons this file forked: left | 1356 | # the leak sweep's business, but they are daemons this file forked: left |
| 1340 | # alive they outlive the suite holding a private key, which is the one | 1357 | # alive they outlive the suite holding a private key, which is the one |
| @@ -1387,6 +1404,7 @@ cleanup() { | |||
| 1387 | [ -S "$SOCK59" ] && "$MUXD" stop --sock "$SOCK59" 2>/dev/null || true | 1404 | [ -S "$SOCK59" ] && "$MUXD" stop --sock "$SOCK59" 2>/dev/null || true |
| 1388 | [ -S "$SOCK60" ] && "$MUXD" stop --sock "$SOCK60" 2>/dev/null || true | 1405 | [ -S "$SOCK60" ] && "$MUXD" stop --sock "$SOCK60" 2>/dev/null || true |
| 1389 | [ -S "$SOCK61" ] && "$MUXD" stop --sock "$SOCK61" 2>/dev/null || true | 1406 | [ -S "$SOCK61" ] && "$MUXD" stop --sock "$SOCK61" 2>/dev/null || true |
| 1407 | [ -S "$SOCK64" ] && "$MUXD" stop --sock "$SOCK64" 2>/dev/null || true | ||
| 1390 | 1408 | ||
| 1391 | # ---- the leak sweep (hygiene kit, 6a) ---- | 1409 | # ---- the leak sweep (hygiene kit, 6a) ---- |
| 1392 | # Here rather than at the bottom of the file, which `set -e` reaches only | 1410 | # Here rather than at the bottom of the file, which `set -e` reaches only |
| @@ -1403,7 +1421,7 @@ cleanup() { | |||
| 1403 | "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ | 1421 | "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ |
| 1404 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \ | 1422 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \ |
| 1405 | "$D55PID" "$D56PID" "$D57PID" "$D58PID" "$D59PID" \ | 1423 | "$D55PID" "$D56PID" "$D57PID" "$D58PID" "$D59PID" \ |
| 1406 | "$D60PID" "$D61PID" "$D62PID" "$D63PID" "$D64PID" | 1424 | "$D60PID" "$D61PID" "$D62PID" "$D63PID" "$D64PID" "$D65PID" |
| 1407 | _leak=0 | 1425 | _leak=0 |
| 1408 | leak_sweep "$_rc" || _leak=1 | 1426 | leak_sweep "$_rc" || _leak=1 |
| 1409 | 1427 | ||
| @@ -1499,7 +1517,11 @@ cleanup() { | |||
| 1499 | "$OUT.lpdfa" "$OUT.lpdstop" "$SOCK61" \ | 1517 | "$OUT.lpdfa" "$OUT.lpdstop" "$SOCK61" \ |
| 1500 | "$OUT.pra.d" "$OUT.prb.d" "$OUT.pra" "$OUT.pra.err" "$OUT.prcap" "$OUT.prcap.err" \ | 1518 | "$OUT.pra.d" "$OUT.prb.d" "$OUT.pra" "$OUT.pra.err" "$OUT.prcap" "$OUT.prcap.err" \ |
| 1501 | "$OUT.prgrid" \ | 1519 | "$OUT.prgrid" \ |
| 1502 | "$OUT.prpc" "$OUT.prfb" "$OUT.prastop" "$OUT.prbstop" "$SOCK62" "$SOCK63" | 1520 | "$OUT.prpc" "$OUT.prfb" "$OUT.prastop" "$OUT.prbstop" "$SOCK62" "$SOCK63" \ |
| 1521 | "$OUT.hyd.d" "$OUT.hydcap" "$OUT.hydcap.err" "$OUT.hydpc" "$OUT.hydgrid" \ | ||
| 1522 | "$OUT.hydsta" "$OUT.hydstb" "$OUT.hydghost" "$OUT.hydpre" "$OUT.hydfa" \ | ||
| 1523 | "$OUT.hydstop" "$OUT.hydfcap" "$OUT.hydfcap.err" "$OUT.hydfpc" \ | ||
| 1524 | "$OUT.hydfold" "$OUT.hydfgrid" "$SOCK64" "$HYKEY" | ||
| 1503 | # ...and the non-tty capture that leg's session feeds. | 1525 | # ...and the non-tty capture that leg's session feeds. |
| 1504 | rm -f "$OUT.nogate" | 1526 | rm -f "$OUT.nogate" |
| 1505 | # ...and its other half: the paste capture and the file nvim wrote, which | 1527 | # ...and its other half: the paste capture and the file nvim wrote, which |
| @@ -8510,8 +8532,167 @@ D64PID="" | |||
| 8510 | rm -rf "$PRSTATE" | 8532 | rm -rf "$PRSTATE" |
| 8511 | ok "Ctrl-\\ : adds a tile by spelling: born on another daemon, recorded, refusals narrated, Esc eats the line" | 8533 | ok "Ctrl-\\ : adds a tile by spelling: born on another daemon, recorded, refusals narrated, Esc eats the line" |
| 8512 | 8534 | ||
| 8513 | [ "$OK_COUNT" = "73" ] || { | 8535 | # ---- a saved local line comes back as a fresh session ------------------- |
| 8514 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 73 —" | 8536 | # |
| 8537 | # The wall file is attach history, and the daemon that held those sessions | ||
| 8538 | # dies on every reboot. Joining only meant the wall came back as a grid of | ||
| 8539 | # [refused] tiles nobody could do anything with but forget them by hand. | ||
| 8540 | # A restored line naming the LOCAL socket now attaches-or-creates. | ||
| 8541 | # | ||
| 8542 | # The remote half is the same wall, the same run and the same daemon, | ||
| 8543 | # reached the other way: a `quic://` line for a session that does not | ||
| 8544 | # exist stays [refused] and creates nothing. Recreating a shell on your | ||
| 8545 | # own box is cheap and expected; spawning one on another host out of a | ||
| 8546 | # saved file is not, and the split is the whole ruling. | ||
| 8547 | # | ||
| 8548 | # Daemon truth, not paint, answers "was it created": `muxa status` on a | ||
| 8549 | # name the daemon does not have exits non-zero, which is the same probe | ||
| 8550 | # the leg runs BEFORE the wall so it cannot pass vacuously. The render | ||
| 8551 | # oracle answers the other question — what the human was looking at — | ||
| 8552 | # because a bar left saying [refused] is invisible to a grep of the | ||
| 8553 | # emitted bytes. | ||
| 8554 | # | ||
| 8555 | # Both roads a restored line can take are here, because they are two call | ||
| 8556 | # sites of the one rule: the no-argv `mux wall` below runs the startup tile | ||
| 8557 | # loop, and the `Ctrl-\ w` half at the end of this leg runs `hydrate()`. | ||
| 8558 | head -c 32 /dev/urandom > "$HYKEY" | ||
| 8559 | chmod 600 "$HYKEY" | ||
| 8560 | "$MUXD" run --sock "$SOCK64" --shell /bin/sh \ | ||
| 8561 | --quic "127.0.0.1:$HYPORT" --key "$HYKEY" --quic-idle-ms 15000 > "$OUT.hyd.d" 2>&1 & | ||
| 8562 | D65PID=$! | ||
| 8563 | wait_sock "$SOCK64" "$OUT.hyd.d" "hydrate-create daemon never bound" | ||
| 8564 | |||
| 8565 | # The wall file names three sessions this daemon does not have. Written by | ||
| 8566 | # hand rather than earned by an attach, because an attach that earned the | ||
| 8567 | # line would also have created the session — and then there would be | ||
| 8568 | # nothing for the restore to create. | ||
| 8569 | mkdir -p "$HYSTATE/mux" | ||
| 8570 | printf -- '--sock %s#hya\n--sock %s#hyb\nquic://127.0.0.1:%s#hyghost\n' \ | ||
| 8571 | "$SOCK64" "$SOCK64" "$HYPORT" > "$HYWALL" | ||
| 8572 | |||
| 8573 | # Vacuity guard: none of the three exists yet. A leg whose daemon already | ||
| 8574 | # had them would pass without the restore creating anything. | ||
| 8575 | for _hy in hya hyb hyghost; do | ||
| 8576 | timeout 20 "$MUXA" status --sock "$SOCK64" --session "$_hy" > "$OUT.hydpre" 2>&1 && { | ||
| 8577 | echo "e2e FAIL: hydrate-create: session $_hy existed before the wall ran:" | ||
| 8578 | cat "$OUT.hydpre"; exit 1; } | ||
| 8579 | done | ||
| 8580 | |||
| 8581 | # 70x36: under the aspect rule (70 < 2*36) the cut is stacked, so each of | ||
| 8582 | # the three tiles gets the full width and its label bar survives the | ||
| 8583 | # render intact — the socket path is in that label. | ||
| 8584 | set +e | ||
| 8585 | XDG_STATE_HOME="$HYSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \ | ||
| 8586 | --out "$OUT.hydcap" --err "$OUT.hydcap.err" -- \ | ||
| 8587 | "$MUX" wall --key "$HYKEY" > "$OUT.hydpc" 2>&1 <<'EOF' | ||
| 8588 | expect [up] 25000 | ||
| 8589 | settle 1000 25000 | ||
| 8590 | send printf 'hyd-%s\n' marker\n | ||
| 8591 | expect hyd-marker 15000 | ||
| 8592 | settle 600 15000 | ||
| 8593 | send \x1cd | ||
| 8594 | waitexit 10000 | ||
| 8595 | EOF | ||
| 8596 | RC=$? | ||
| 8597 | set -e | ||
| 8598 | [ "$RC" -eq 0 ] || { | ||
| 8599 | echo "e2e FAIL: hydrate-create: ptyclient exited $RC:" | ||
| 8600 | cat "$OUT.hydpc" "$OUT.hydcap.err"; exit 1; } | ||
| 8601 | |||
| 8602 | # The local lines created their sessions, at the width of the stripe that | ||
| 8603 | # created them. The width does not discriminate create from join on its own | ||
| 8604 | # (a join's resize doorbell lands at 70 too, and the daemon refuses a create | ||
| 8605 | # under min_session_cols) — the muxa exit code above is what says created. | ||
| 8606 | # It is here to catch a session that came up at some OTHER client's size. | ||
| 8607 | timeout 20 "$MUXA" status --sock "$SOCK64" --session hya > "$OUT.hydsta" 2>&1 || { | ||
| 8608 | echo "e2e FAIL: hydrate-create: the saved line for hya did not create it:" | ||
| 8609 | cat "$OUT.hydsta"; exit 1; } | ||
| 8610 | grep -q '"cols":70' "$OUT.hydsta" || { | ||
| 8611 | echo "e2e FAIL: hydrate-create: hya exists but not at the tile's width:" | ||
| 8612 | cat "$OUT.hydsta"; exit 1; } | ||
| 8613 | timeout 20 "$MUXA" status --sock "$SOCK64" --session hyb > "$OUT.hydstb" 2>&1 || { | ||
| 8614 | echo "e2e FAIL: hydrate-create: the saved line for hyb did not create it:" | ||
| 8615 | cat "$OUT.hydstb"; exit 1; } | ||
| 8616 | |||
| 8617 | # ...and the created session runs a real shell: the marker typed into the | ||
| 8618 | # focused tile (tile 0, the first wall line) is the shell's own work. | ||
| 8619 | timeout 20 "$MUXA" capture --sock "$SOCK64" --session hya > "$OUT.hydfa" 2>&1 | ||
| 8620 | grep -q "hyd-marker" "$OUT.hydfa" || { | ||
| 8621 | echo "e2e FAIL: hydrate-create: the marker never reached the created session:" | ||
| 8622 | cat "$OUT.hydfa"; exit 1; } | ||
| 8623 | |||
| 8624 | # The remote line created NOTHING. Same daemon, same run, same file — only | ||
| 8625 | # the spelling differs. | ||
| 8626 | timeout 20 "$MUXA" status --sock "$SOCK64" --session hyghost > "$OUT.hydghost" 2>&1 && { | ||
| 8627 | echo "e2e FAIL: hydrate-create: a quic:// wall line created a session:" | ||
| 8628 | cat "$OUT.hydghost"; exit 1; } | ||
| 8629 | |||
| 8630 | # What the human saw: two tiles up, the remote one still refused. | ||
| 8631 | "$RENDER" --cols 70 --rows 36 < "$OUT.hydcap" > "$OUT.hydgrid" || { | ||
| 8632 | echo "e2e FAIL: hydrate-create: render oracle failed" | ||
| 8633 | cat "$OUT.hydgrid"; exit 1; } | ||
| 8634 | grep -q -- "#hya \[up\]" "$OUT.hydgrid" || { | ||
| 8635 | echo "e2e FAIL: hydrate-create: the hya tile never said [up]:" | ||
| 8636 | cat "$OUT.hydgrid"; exit 1; } | ||
| 8637 | grep -q -- "#hyb \[up\]" "$OUT.hydgrid" || { | ||
| 8638 | echo "e2e FAIL: hydrate-create: the hyb tile never said [up]:" | ||
| 8639 | cat "$OUT.hydgrid"; exit 1; } | ||
| 8640 | grep -q -- "#hyghost \[refused\]" "$OUT.hydgrid" || { | ||
| 8641 | echo "e2e FAIL: hydrate-create: the remote tile did not say [refused]:" | ||
| 8642 | cat "$OUT.hydgrid"; exit 1; } | ||
| 8643 | # The OTHER road a restored line takes: `Ctrl-\ w`. A no-argv `mux wall` | ||
| 8644 | # runs the startup tile loop; the fold runs `hydrate()`, a different call | ||
| 8645 | # site of the same rule, and the ruling covers both. Same daemon, same | ||
| 8646 | # state home, one more ptyclient — a `mux TARGET` is the entry tile, and | ||
| 8647 | # the fold brings the rest of the file in around it. | ||
| 8648 | # | ||
| 8649 | # The file is rewritten to name a session this daemon does not have. The | ||
| 8650 | # entry attach then records its OWN line into the same file (attach | ||
| 8651 | # history), which the fold dedupes by spelling — so what the fold has left | ||
| 8652 | # to add is exactly the absent one, and creating it is the whole claim. | ||
| 8653 | printf -- '--sock %s#hyfold\n' "$SOCK64" > "$HYWALL" | ||
| 8654 | timeout 20 "$MUXA" status --sock "$SOCK64" --session hyfold > "$OUT.hydpre" 2>&1 && { | ||
| 8655 | echo "e2e FAIL: hydrate-create: session hyfold existed before the fold ran:" | ||
| 8656 | cat "$OUT.hydpre"; exit 1; } | ||
| 8657 | |||
| 8658 | set +e | ||
| 8659 | XDG_STATE_HOME="$HYSTATE" timeout 90 "$PTYCLIENT" --cols 70 --rows 36 \ | ||
| 8660 | --out "$OUT.hydfcap" --err "$OUT.hydfcap.err" -- \ | ||
| 8661 | "$MUX" --sock "$SOCK64" --session hya > "$OUT.hydfpc" 2>&1 <<'EOF' | ||
| 8662 | settle 1000 25000 | ||
| 8663 | send printf 'hydf-%s\n' pre\n | ||
| 8664 | expect hydf-pre 20000 | ||
| 8665 | settle 600 20000 | ||
| 8666 | send \x1cw | ||
| 8667 | settle 1500 25000 | ||
| 8668 | send \x1cd | ||
| 8669 | waitexit 10000 | ||
| 8670 | EOF | ||
| 8671 | RC=$? | ||
| 8672 | set -e | ||
| 8673 | [ "$RC" -eq 0 ] || { | ||
| 8674 | echo "e2e FAIL: hydrate-create: fold ptyclient exited $RC:" | ||
| 8675 | cat "$OUT.hydfpc" "$OUT.hydfcap.err"; exit 1; } | ||
| 8676 | |||
| 8677 | # Daemon truth again: the fold-born tile created the session its line named. | ||
| 8678 | timeout 20 "$MUXA" status --sock "$SOCK64" --session hyfold > "$OUT.hydfold" 2>&1 || { | ||
| 8679 | echo "e2e FAIL: hydrate-create: the folded line for hyfold did not create it:" | ||
| 8680 | cat "$OUT.hydfold"; exit 1; } | ||
| 8681 | # ...and it is a tile on the screen, not merely a session on the daemon. | ||
| 8682 | "$RENDER" --cols 70 --rows 36 < "$OUT.hydfcap" > "$OUT.hydfgrid" || { | ||
| 8683 | echo "e2e FAIL: hydrate-create: fold render oracle failed" | ||
| 8684 | cat "$OUT.hydfgrid"; exit 1; } | ||
| 8685 | grep -q -- "#hyfold \[up\]" "$OUT.hydfgrid" || { | ||
| 8686 | echo "e2e FAIL: hydrate-create: the fold-born tile never said [up]:" | ||
| 8687 | cat "$OUT.hydfgrid"; exit 1; } | ||
| 8688 | |||
| 8689 | assert_stopped "$SOCK64" "$D65PID" "hydrate-create" "$OUT.hydstop" | ||
| 8690 | D65PID="" | ||
| 8691 | rm -rf "$HYSTATE" | ||
| 8692 | ok "a saved local line comes back as a fresh session, by wall and by fold; a remote one joins only" | ||
| 8693 | |||
| 8694 | [ "$OK_COUNT" = "74" ] || { | ||
| 8695 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 74 —" | ||
| 8515 | echo " a scenario was added (update the pin) or silently lost" | 8696 | echo " a scenario was added (update the pin) or silently lost" |
| 8516 | exit 1 | 8697 | exit 1 |
| 8517 | } | 8698 | } |