da3ffc01
fix: tp2b syncs on a width witness — a scrolling repaint reorders rows across deltas
a73x 2026-08-10 11:16
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -1613,15 +1613,16 @@ ok "ptyclient controls: pty echo roundtrips, impossible expect fails loudly, std | |||
| 1613 | # resize-on-prefix guard never fires. Verified by probe, not by reading. | 1613 | # resize-on-prefix guard never fires. Verified by probe, not by reading. |
| 1614 | # | 1614 | # |
| 1615 | # tp2b is the leg that catches row 7 (the prefix ignored), and it only | 1615 | # tp2b is the leg that catches row 7 (the prefix ignored), and it only |
| 1616 | # catches it because of WHERE the damage can show. A delta paints its | 1616 | # catches it because of WHERE the damage can show. DELTA STREAMING |
| 1617 | # rows straight to the tty, absolutely addressed and clipped to the tty, | 1617 | # bypasses the replica: those rows go straight to the tty, absolutely |
| 1618 | # so a stale replica is invisible on that path and a wrong row is healed | 1618 | # addressed and clipped to the tty, so a stale replica cannot spoil them |
| 1619 | # by the next delta that re-addresses it. The one moment the client paints | 1619 | # and a wrong row is healed by the next delta that re-addresses it. The |
| 1620 | # FROM the replica is the full repaint that answers a snapshot, clipped to | 1620 | # damage therefore has to survive to a FULL REPAINT, which is the paint |
| 1621 | # min(replica, tty) — so the damage has to be still ON SCREEN then, and in | 1621 | # that reads the replica back out and clips at min(replica, tty) — still |
| 1622 | # a region the stale geometry cannot reach. Hence: fill the screen, put a | 1622 | # on screen by then, and in a region the stale geometry cannot reach. |
| 1623 | # 95-wide row on it, then winch UP to 100x30. With the prefix ignored the | 1623 | # Hence: fill the screen, put a 95-wide row on it, then winch UP to |
| 1624 | # replica stays 90x28 and that repaint reproduces the OLD geometry. | 1624 | # 100x30. With the prefix ignored the replica stays 90x28 and the repaint |
| 1625 | # answering the resize reproduces the OLD geometry. | ||
| 1625 | "$MUXD" run --sock "$SOCK12" --shell /bin/sh > "$OUT.tp2.d" 2>&1 & | 1626 | "$MUXD" run --sock "$SOCK12" --shell /bin/sh > "$OUT.tp2.d" 2>&1 & |
| 1626 | D12PID=$! | 1627 | D12PID=$! |
| 1627 | i=0 | 1628 | i=0 |
| @@ -1679,6 +1680,37 @@ assert_converged "$OUT.tp2a" "$SOCK12" "pty attach at 100x30" 100 30 | |||
| 1679 | # repainted, including by the echo of a command typed at it, so each | 1680 | # repainted, including by the echo of a command typed at it, so each |
| 1680 | # trailing prompt is consumed BEFORE the resize. That leaves the expect | 1681 | # trailing prompt is consumed BEFORE the resize. That leaves the expect |
| 1681 | # after `resize` satisfiable only by the answering snapshot's repaint. | 1682 | # after `resize` satisfiable only by the answering snapshot's repaint. |
| 1683 | # | ||
| 1684 | # THE HAZARD, which cost a 1-in-21 hang before it was understood: on a | ||
| 1685 | # FULL screen the output scrolls, and the daemon can flush the new prompt | ||
| 1686 | # row in one delta and the scrolled rows in a LATER one. A sentinel can | ||
| 1687 | # therefore reach the byte stream ~600 bytes BEFORE an output needle that | ||
| 1688 | # precedes it in this script. Any `expect NEEDLE` followed by `expect | ||
| 1689 | # SENTINEL` can then deadlock: the needle's match consumes the cursor past | ||
| 1690 | # the sentinel, and the sentinel expect waits on a shell that has gone | ||
| 1691 | # idle — a hang, on a client whose grid was right all along. So no expect | ||
| 1692 | # here may assume that one ROW reached the stream before another. | ||
| 1693 | # | ||
| 1694 | # What this scenario must NOT do to fix that is replace `expect 00007` | ||
| 1695 | # with a second sentinel. Measured against the row-7 mutation: 00007 is | ||
| 1696 | # 10/10 red, doubled sentinels 1/10. The reason is that a sentinel is | ||
| 1697 | # satisfied by any repaint of the prompt row, including the ones the | ||
| 1698 | # scroll itself emits, so the resize fires before the wide row's deltas | ||
| 1699 | # have landed and the later deltas — which bypass the replica — heal the | ||
| 1700 | # damage before the repaint can expose it. `expect 00007` is what holds | ||
| 1701 | # the resize until the wide row is actually ON the screen it will be | ||
| 1702 | # repainted from. | ||
| 1703 | # | ||
| 1704 | # The post-resize needle is 91 zeros, and it is a WIDTH WITNESS rather | ||
| 1705 | # than a sync point: at 90 columns the wide row is painted wrapped, so the | ||
| 1706 | # longest contiguous run the client can emit is 90, and only a paint at | ||
| 1707 | # the reflowed width can produce 91. That makes it immune to the hazard | ||
| 1708 | # above (it cannot have arrived before the resize, whatever order the rows | ||
| 1709 | # came in) and a discriminator in its own right — under the mutation it | ||
| 1710 | # never arrives and the leg fails on the fixture's timeout in ~10s rather | ||
| 1711 | # than on the convergence diff. The trailing sentinel is safe where it now | ||
| 1712 | # sits: a repaint emits its rows top to bottom, so the prompt row follows | ||
| 1713 | # the wide row within the very paint the witness just matched. | ||
| 1682 | set +e | 1714 | set +e |
| 1683 | timeout 40 "$PTYCLIENT" --cols 90 --rows 28 --out "$OUT.tp2b" --err "$OUT.tp2b.err" -- \ | 1715 | timeout 40 "$PTYCLIENT" --cols 90 --rows 28 --out "$OUT.tp2b" --err "$OUT.tp2b.err" -- \ |
| 1684 | "$MUX" --sock "$SOCK12" > "$OUT.tp2b.log" 2>&1 <<'EOF' | 1716 | "$MUX" --sock "$SOCK12" > "$OUT.tp2b.log" 2>&1 <<'EOF' |
| @@ -1688,8 +1720,8 @@ expect fill-done 10000 | |||
| 1688 | expect tp2rdy@ 10000 | 1720 | expect tp2rdy@ 10000 |
| 1689 | send printf '%095d\\n' 7\n | 1721 | send printf '%095d\\n' 7\n |
| 1690 | expect 00007 10000 | 1722 | expect 00007 10000 |
| 1691 | expect tp2rdy@ 10000 | ||
| 1692 | resize 100 30 | 1723 | resize 100 30 |
| 1724 | expect 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 10000 | ||
| 1693 | expect tp2rdy@ 10000 | 1725 | expect tp2rdy@ 10000 |
| 1694 | send \x1c | 1726 | send \x1c |
| 1695 | waitexit 10000 | 1727 | waitexit 10000 |
| @@ -1699,14 +1731,16 @@ set -e | |||
| 1699 | [ "$RC" -eq 0 ] || { | 1731 | [ "$RC" -eq 0 ] || { |
| 1700 | echo "e2e FAIL: tp2b ptyclient exited $RC:"; cat "$OUT.tp2b.log"; exit 1; } | 1732 | echo "e2e FAIL: tp2b ptyclient exited $RC:"; cat "$OUT.tp2b.log"; exit 1; } |
| 1701 | # Graded against the mutation (row 7: the prefix resize commented out), | 1733 | # Graded against the mutation (row 7: the prefix resize commented out), |
| 1702 | # and this is what the mutated build actually did — two divergences in one | 1734 | # 10 runs out of 10 red. What the mutated build does now is fail ABOVE, |
| 1703 | # diff, neither of them an expect timeout, since every needle still | 1735 | # on the width witness: the 91-zero needle never arrives, so the fixture |
| 1704 | # arrives on a stale replica: | 1736 | # times out at that verb and the RC check fires. Before the witness |
| 1705 | # the 30-row grid came back with its top rows missing, because a 28-row | 1737 | # existed the same build reached this line and failed here instead, with |
| 1706 | # replica has 28 rows to repaint from; | 1738 | # both halves of the stale geometry in one diff — the 30-row grid coming |
| 1707 | # the 95-wide row came back still WRAPPED at 90 into "...0" + "00007", | 1739 | # back short its top rows because a 28-row replica has only 28 to repaint |
| 1708 | # because the replica that reflowed it is still 90 wide, while the | 1740 | # from, and the 95-wide row coming back still WRAPPED at 90 into "...0" + |
| 1709 | # daemon had already rejoined it into one 95-char row at 100. | 1741 | # "00007" while the daemon had rejoined it at 100. Both checks are kept: |
| 1742 | # the witness names the width directly and fails in ~10s, convergence | ||
| 1743 | # still covers everything the witness does not look at. | ||
| 1710 | assert_converged "$OUT.tp2b" "$SOCK12" "pty resize mid-session" 100 30 | 1744 | assert_converged "$OUT.tp2b" "$SOCK12" "pty resize mid-session" 100 30 |
| 1711 | rm -f "$OUT.tp2a" "$OUT.tp2a.err" "$OUT.tp2a.log" \ | 1745 | rm -f "$OUT.tp2a" "$OUT.tp2a.err" "$OUT.tp2a.log" \ |
| 1712 | "$OUT.tp2b" "$OUT.tp2b.err" "$OUT.tp2b.log" "$OUT.tp2.d" | 1746 | "$OUT.tp2b" "$OUT.tp2b.err" "$OUT.tp2b.log" "$OUT.tp2.d" |