cb1e4946
test: the mutation notes name what the mutants actually did
a73x 2026-08-20 07:55
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -4702,12 +4702,22 @@ while `\x1c w` is a chord — so the abort is judged after `feed`, not before it | |||
| 4702 | Secondary, from the same review: the sentence was wrong for half the states it | 4702 | Secondary, from the same review: the sentence was wrong for half the states it |
| 4703 | covered. Nothing has been "detached while reconnecting" from a first attach | 4703 | covered. Nothing has been "detached while reconnecting" from a first attach |
| 4704 | that never came up, so `.connecting` says `aborted before attaching` instead — | 4704 | that never came up, so `.connecting` says `aborted before attaching` instead — |
| 4705 | which is also what `openFailure` says for an abort one layer down. | 4705 | which is also what `openFailure` says for an abort one layer down. — and both had to be re-aimed, |
| 4706 | 4706 | which is the finding worth recording rather than the kills. | |
| 4707 | Two mutations, to check the new legs have teeth. Making a ring step re-dial a | 4707 | |
| 4708 | sibling that already has a tile fails `ring grow` at `the daemon accepted 3 | 4708 | Making a ring step re-dial a sibling that already has a tile kills the SUITE at |
| 4709 | attaches (want 2)` — the flat-count assertion, and nothing else in the suite | 4709 | `zoom skip`, four legs before `ring grow` ever runs: the same flat-count |
| 4710 | notices. Making a single-tile session end drop to the wall instead of exiting | 4710 | assertion, on the phase-1 path, and it gets there first. Run against the |
| 4711 | fails `exit semantics` at `the only tile's shell exited 7 and mux exited 124`, | 4711 | `ring grow` leg standalone the mutant gives `delta=3 want=2`, which is the |
| 4712 | which is the timeout: a wall nobody can leave is exactly what that half of the | 4712 | assertion that leg was built for. Making a single-tile session end drop to the |
| 4713 | rule exists to refuse, and the 124 says so in the failure's own words. | 4713 | wall instead of exiting is worse: it wedges an early piped leg and the suite |
| 4714 | stops having proved nothing about the rule at all. Standalone, `exit 7` under | ||
| 4715 | the mutant comes back `0` rather than `7` — not the timeout's 124 that the | ||
| 4716 | obvious reading predicts, because the wall it dropped to reads the script's | ||
| 4717 | closed stdin as a wall nobody is left to steer and leaves. Zero is not seven, | ||
| 4718 | so the leg fires either way, but the failure text had been written for the 124 | ||
| 4719 | and now names both shapes. | ||
| 4720 | |||
| 4721 | The rule this cost re-learning is the one already in these notes: a mutation | ||
| 4722 | that fails the suite somewhere is not a mutation that fails the leg you aimed | ||
| 4723 | it at. When the two differ, go get the targeted answer, and quote THAT. | ||
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -1224,7 +1224,8 @@ cleanup() { | |||
| 1224 | "$OUT.rgcap" "$OUT.rgcap.err" "$OUT.rgpc" "$OUT.rgcapt" "$OUT.rgcap0" \ | 1224 | "$OUT.rgcap" "$OUT.rgcap.err" "$OUT.rgpc" "$OUT.rgcapt" "$OUT.rgcap0" \ |
| 1225 | "$OUT.rgstop" | 1225 | "$OUT.rgstop" |
| 1226 | rm -f "$SOCK47" "$OUT.xe.d" "$OUT.xe1" "$OUT.xe1.err" \ | 1226 | rm -f "$SOCK47" "$OUT.xe.d" "$OUT.xe1" "$OUT.xe1.err" \ |
| 1227 | "$OUT.xecap" "$OUT.xecap.err" "$OUT.xepc" "$OUT.xestop" | 1227 | "$OUT.xecap" "$OUT.xecap.err" "$OUT.xepc" "$OUT.xe2" "$OUT.xe2.err" \ |
| 1228 | "$OUT.xestop" | ||
| 1228 | rm -rf "$CVSTATE" "$RGSTATE" "$RGOTHER" "$XESTATE" | 1229 | rm -rf "$CVSTATE" "$RGSTATE" "$RGOTHER" "$XESTATE" |
| 1229 | # The convergence files a FAILING assert_converged leaves behind | 1230 | # The convergence files a FAILING assert_converged leaves behind |
| 1230 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not | 1231 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not |
| @@ -6747,14 +6748,19 @@ ok "Ctrl-\\ n grows the wall for a sibling with no tile, and is free for one tha | |||
| 6747 | # Both halves in one block, because either alone is satisfied by a bug: | 6748 | # Both halves in one block, because either alone is satisfied by a bug: |
| 6748 | # | 6749 | # |
| 6749 | # * one tile. `exit 7` in a piped `mux` — the shape every script in the | 6750 | # * one tile. `exit 7` in a piped `mux` — the shape every script in the |
| 6750 | # world uses — and the code comes back 7. A wall that always dropped to | 6751 | # world uses — and the code comes back 7. Graded against the mutation |
| 6751 | # itself would hang here forever, which is why the timeout's 124 is a | 6752 | # (`endAction` dropping to the wall whatever the tile count): `mux` |
| 6752 | # failure this leg names. | 6753 | # comes back 0, because the wall it dropped to reads this script's |
| 6754 | # closed stdin as a wall nobody is left to steer. Zero is not seven and | ||
| 6755 | # the leg says so; 124 is the other shape the same bug can take. | ||
| 6753 | # * two tiles. `\x1cc` adds one and zooms it, the shell in it exits, and | 6756 | # * two tiles. `\x1cc` adds one and zooms it, the shell in it exits, and |
| 6754 | # mux is still running, on the wall, with the dead tile narrating | 6757 | # mux is still running, on the wall, with the dead tile narrating |
| 6755 | # `[exited]` beside the live one. Then `q` leaves — exit 0, not 7 and | 6758 | # `[exited]` beside the live one. Then `q` leaves — exit 0, not 7 and |
| 6756 | # not the shell's anything, because leaving the wall is the user's act | 6759 | # not the shell's anything, because leaving the wall is the user's act |
| 6757 | # and not the session's. | 6760 | # and not the session's. |
| 6761 | # * two tiles and a PIPED client whose stdin has already gone. The case | ||
| 6762 | # review reproduced as a hang, and the reason the rule's second half is | ||
| 6763 | # stated in `endAction` rather than at the read that notices EOF. | ||
| 6758 | "$MUXD" run --sock "$SOCK47" --shell /bin/sh > "$OUT.xe.d" 2>&1 & | 6764 | "$MUXD" run --sock "$SOCK47" --shell /bin/sh > "$OUT.xe.d" 2>&1 & |
| 6759 | D47PID=$! | 6765 | D47PID=$! |
| 6760 | wait_sock "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound" | 6766 | wait_sock "$SOCK47" "$OUT.xe.d" "exit-semantics daemon never bound" |
| @@ -6766,7 +6772,8 @@ RC=$? | |||
| 6766 | set -e | 6772 | set -e |
| 6767 | [ "$RC" -eq 7 ] || { | 6773 | [ "$RC" -eq 7 ] || { |
| 6768 | echo "e2e FAIL: exit semantics: the only tile's shell exited 7 and mux exited $RC" | 6774 | echo "e2e FAIL: exit semantics: the only tile's shell exited 7 and mux exited $RC" |
| 6769 | echo " (124 means it dropped to a wall nobody could leave):" | 6775 | echo " — a wall it dropped to instead reads as 0 once this script's" |
| 6776 | echo " stdin closes under it, or 124 if it never gets that far:" | ||
| 6770 | cat "$OUT.xe1.err"; exit 1; } | 6777 | cat "$OUT.xe1.err"; exit 1; } |
| 6771 | 6778 | ||
| 6772 | set +e | 6779 | set +e |
| @@ -6806,6 +6813,38 @@ grep -q -- "--sock $SOCK47#0 \[up\]" "$OUT.xecap" || { | |||
| 6806 | # The session that did NOT end is still there: a shell exiting under the | 6813 | # The session that did NOT end is still there: a shell exiting under the |
| 6807 | # zoom ends that session and nothing else. | 6814 | # zoom ends that session and nothing else. |
| 6808 | wait_grid "$SOCK47" "xehome-pin" "exit semantics: session 0 outlived its neighbour" | 6815 | wait_grid "$SOCK47" "xehome-pin" "exit semantics: session 0 outlived its neighbour" |
| 6816 | |||
| 6817 | # The third case, and the one review REPRODUCED as a hang: two tiles, but | ||
| 6818 | # a PIPED client whose stdin has already closed when the zoomed session | ||
| 6819 | # ends. "Drop to the wall" is not an answer there — the wall it drops to | ||
| 6820 | # has no keyboard, so mux waited in poll forever with the exit code in | ||
| 6821 | # hand (RC=124, measured). The rule is the same rule with its second half | ||
| 6822 | # stated: an ending nobody can leave is mux's ending. | ||
| 6823 | # | ||
| 6824 | # The gap is built rather than raced: the shell reads `sleep 4` and | ||
| 6825 | # `exit 3` from a stdin that closes IMMEDIATELY after, so the pipe is long | ||
| 6826 | # gone by the time the session ends four seconds later. `timeout` is what | ||
| 6827 | # turns a regression into a failure instead of a wedged suite. | ||
| 6828 | XETIME_START=$(date +%s) | ||
| 6829 | set +e | ||
| 6830 | { sleep 1; printf '\034c'; sleep 3; printf 'sleep 4\n'; printf 'exit 3\n'; } | \ | ||
| 6831 | XDG_STATE_HOME="$XESTATE" timeout 40 "$MUX" --sock "$SOCK47" \ | ||
| 6832 | > "$OUT.xe2" 2> "$OUT.xe2.err" | ||
| 6833 | RC=$? | ||
| 6834 | set -e | ||
| 6835 | XETIME=$(( $(date +%s) - XETIME_START )) | ||
| 6836 | [ "$RC" -eq 3 ] || { | ||
| 6837 | echo "e2e FAIL: exit semantics: a piped two-tile client whose session ended" | ||
| 6838 | echo " exited $RC after ${XETIME}s, want 3 — 124 is the wall it" | ||
| 6839 | echo " dropped to with nobody left to leave it:" | ||
| 6840 | cat "$OUT.xe2.err"; exit 1; } | ||
| 6841 | # Promptly, not eventually: the code is right in both readings, but a | ||
| 6842 | # client that only exits because the timeout reaped it is the bug wearing | ||
| 6843 | # the right answer. The session ends ~8s in; 20 is generous and still far | ||
| 6844 | # under the 40s timeout that would otherwise be the only bound. | ||
| 6845 | [ "$XETIME" -lt 20 ] || { | ||
| 6846 | echo "e2e FAIL: exit semantics: the piped client took ${XETIME}s to notice its" | ||
| 6847 | echo " own session had ended — it exited, but it did not notice"; exit 1; } | ||
| 6809 | assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop" | 6848 | assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop" |
| 6810 | D47PID="" | 6849 | D47PID="" |
| 6811 | ok "a session ending under the zoom exits mux alone, and drops to the wall beside company" | 6850 | ok "a session ending under the zoom exits mux alone, and drops to the wall beside company" |