a73x

e09da54f

fix: review conditions — soak refuses a dirty baseline; the scroll bug's true severity

a73x   2026-08-09 21:47

Commit message
fix: review conditions — soak refuses a dirty baseline; the scroll bug's true severity

The reviewer ran five sampled mutations and all held; two conditions
folded here. soak.sh charged pre-existing /tmp residue to run 1 of every
soak (green suite, FAIL verdict — the evidence a failing e2e deliberately
leaves is exactly what poisons the next soak), so it now refuses to start
over residue rather than baseline-subtracting it. And the scroll-
suppression bug was overstated everywhere it was described: Shift+PageDown's
scroll_pages==0 arm clears the mode unconditionally, so the state is
recoverable by a keystroke no user would guess, not terminal — comment and
both docs now say so. Also: assert_converged's styled-only failure printed
an empty diff and no explanation (the [ -s ] case is the styled case);
and a drifting line-number cite became a test-name cite.

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

docs/decisions.md
Old New
@@ -1620,8 +1620,11 @@ mode — it drops pending and retired predictions and nothing else. A
1620 reconnect taken while scrolled therefore leaves the client painting live 1620 reconnect taken while scrolled therefore leaves the client painting live
1621 rows with the overlay still suppressing, and the "any other key leaves 1621 rows with the overlay still suppressing, and the "any other key leaves
1622 scroll mode" branch can never rescue it because that branch is guarded by 1622 scroll mode" branch can never rescue it because that branch is guarded by
1623 `scroll_pages > 0`, which the reconnect has just made false. Local echo 1623 `scroll_pages > 0`, which the reconnect has just made false. Shift+PageDown
1624 prediction is off for the rest of the session, silently. Fixed 1624 does still clear it — its `scroll_pages == 0` arm calls
1625 `setScrollMode(false)` unconditionally — so the state is recoverable, by a
1626 keystroke no user has any reason to try. Local echo prediction is silently
1627 off until they do. Fixed
1625 client-side with one call next to the existing `scroll_pages = 0` — 1628 client-side with one call next to the existing `scroll_pages = 0` —
1626 deliberately not by making `flush()` clear the mode, because every 1629 deliberately not by making `flush()` clear the mode, because every
1627 snapshot flushes and a snapshot is not a reason to leave history; the 1630 snapshot flushes and a snapshot is not a reason to leave history; the
@@ -1671,8 +1674,9 @@ forward for another milestone.
1671 pen it could protect against belongs to the HOST terminal — which no 1674 pen it could protect against belongs to the HOST terminal — which no
1672 rendered-grid comparison can ever hold, since the render helper replays 1675 rendered-grid comparison can ever hold, since the render helper replays
1673 into a fresh engine at the default pen by construction. The unit test 1676 into a fresh engine at the default pen by construction. The unit test
1674 at `src/client.zig:1837` is the real guard and fails under the mutation 1677 `paintDeltaClipped skips rows beyond the tty and clamps the cursor`
1675 today; the bank names the dependency guarantee it rests on. 1678 (client.zig) is the real guard and fails under the mutation today; the
1679 bank names the dependency guarantee it rests on.
1676 - **Catch parity is not diagnosis parity** (rows 11 and 13). Both suites 1680 - **Catch parity is not diagnosis parity** (rows 11 and 13). Both suites
1677 catch both mutations, so the disposition column reads "both caught" and 1681 catch both mutations, so the disposition column reads "both caught" and
1678 says nothing interesting. What the columns hide: the old suite fails 1682 says nothing interesting. What the columns hide: the old suite fails
docs/roadmap.md
Old New
@@ -65,8 +65,8 @@ markers; new suite **13 of 18**; the six-row delta is all paint, overlay
65 and clip — the byte-blind class, a wrong picture drawn from right bytes 65 and clip — the byte-blind class, a wrong picture drawn from right bytes
66 (decisions.md, M11, which carries the table in full). Zero rows ended 66 (decisions.md, M11, which carries the table in full). Zero rows ended
67 undecided. En route the campaign found a real bug in unmutated code — a 67 undecided. En route the campaign found a real bug in unmutated code — a
68 reconnect taken while scrolled left prediction silently off for the rest 68 reconnect taken while scrolled left prediction silently off until the
69 of the session — and a computed timing knife-edge in the burst scenario 69 user happened to press Shift+PageDown — and a computed timing knife-edge in the burst scenario
70 that had been sitting on the expiry bound exactly, whose formula also 70 that had been sitting on the expiry bound exactly, whose formula also
71 explains the M9-era `DELAY_MS=400` collapse retroactively. The rule 71 explains the M9-era `DELAY_MS=400` collapse retroactively. The rule
72 extracted: **a timing margin is a number you compute, not a sentence you 72 extracted: **a timing margin is a number you compute, not a sentence you
src/client.zig
Old New
@@ -610,10 +610,14 @@ fn session(
610 // The overlay has to be told, and only the client can tell it: 610 // The overlay has to be told, and only the client can tell it:
611 // `flush()` drops predictions but deliberately leaves the mode 611 // `flush()` drops predictions but deliberately leaves the mode
612 // bit alone, so a reconnect taken while scrolled would leave the 612 // bit alone, so a reconnect taken while scrolled would leave the
613 // overlay suppressing forever. The exit-scroll branch below 613 // overlay suppressing with no page to suppress for. The
614 // cannot rescue it either — that one is guarded by 614 // "any other key" exit at the bottom of the loop cannot rescue
615 // `scroll_pages > 0`, which the line above has just made false. 615 // it — that branch is guarded by `scroll_pages > 0`, which the
616 // Prediction would be silently off for the rest of the session. 616 // line above has just made false. Shift+PageDown still can
617 // (its `scroll_pages == 0` arm clears the mode unconditionally),
618 // so this is recoverable rather than terminal — but only by a
619 // keystroke the user has no reason to guess, so prediction is
620 // silently off until they do.
617 overlay.setScrollMode(false); 621 overlay.setScrollMode(false);
618 if (!reconnect( 622 if (!reconnect(
619 alloc, 623 alloc,
test/e2e.sh
Old New
@@ -167,8 +167,16 @@ assert_converged() {
167 CONV_COUNT=$((CONV_COUNT + 1)) 167 CONV_COUNT=$((CONV_COUNT + 1))
168 converged_quiet "$1" "$2" || { 168 converged_quiet "$1" "$2" || {
169 echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):" 169 echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):"
170 head -40 "$1.diff" 2>/dev/null || \ 170 # An empty diff file is the styled-only case, not a passing one —
171 echo "(no plain diff: divergence is styled-only, or the helper failed)" 171 # the glyphs agree and the pens do not, which is precisely what the
172 # byte-exact leg exists to catch, so say so instead of printing
173 # forty lines of nothing.
174 if [ -s "$1.diff" ]; then
175 head -40 "$1.diff"
176 else
177 echo "(no plain diff: the grids agree on glyphs and differ on STYLE —"
178 echo " compare $1.dvt against $1.rvt)"
179 fi
172 echo "grids left in $1.render / $1.dump / $1.rvt / $1.dvt" 180 echo "grids left in $1.render / $1.dump / $1.rvt / $1.dvt"
173 exit 1 181 exit 1
174 } 182 }
test/soak.sh
Old New
@@ -14,6 +14,19 @@ FAILDIR="$TMP/mux-soak-$$-failures"
14 SUMMARY="$TMP/mux-soak-$$.summary" 14 SUMMARY="$TMP/mux-soak-$$.summary"
15 LOG="$TMP/mux-soak-$$.log" 15 LOG="$TMP/mux-soak-$$.log"
16 : > "$SUMMARY" 16 : > "$SUMMARY"
17 # Residue predating the soak is not this soak's leak. Refuse to start rather
18 # than baseline-subtract: the files are probably evidence a previous failing
19 # e2e deliberately left behind, and the per-run hygiene check below cannot
20 # tell them apart from a leak — forcing a look is better than silently
21 # discarding them.
22 BASE_STRAYS=$(find "$TMP" -maxdepth 1 \( -name 'muxd-e2e-*' -o -name 'mux-e2e-*' \) 2>/dev/null | wc -l)
23 [ "$BASE_STRAYS" -eq 0 ] || {
24 echo "soak FAIL: $BASE_STRAYS mux e2e temp files already in $TMP before run 1."
25 echo " They are probably evidence from an earlier failing e2e."
26 echo " Inspect them, then clear them — the per-run hygiene check"
27 echo " cannot tell them apart from a leak."
28 exit 1
29 }
17 FAILED=0 30 FAILED=0
18 i=1 31 i=1
19 while [ "$i" -le "$N" ]; do 32 while [ "$i" -le "$N" ]; do