a73x

d351788c

test: e2e asserts render-vs-dump convergence, with a control that fires

a73x   2026-08-09 17:20

Commit message
test: e2e asserts render-vs-dump convergence, with a control that fires

test/e2e.sh
Old New
@@ -8,6 +8,8 @@ MUX="$2"
8 # that makes a slow round trip without netem or root. 8 # that makes a slow round trip without netem or root.
9 RAWMODE="$3" 9 RAWMODE="$3"
10 DELAYPIPE="$4" 10 DELAYPIPE="$4"
11 # M11 convergence: replays a client capture into a grid (test/render.zig).
12 RENDER="$5"
11 SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" 13 SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock"
12 OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" 14 OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$"
13 # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, 15 # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none,
@@ -129,6 +131,49 @@ proxy_pid() {
129 ps -eo pid,comm,args | awk -v s="$1" '$2=="muxd" && /proxy/ && index($0,s) {print $1}' | head -1 131 ps -eo pid,comm,args | awk -v s="$1" '$2=="muxd" && /proxy/ && index($0,s) {print $1}' | head -1
130 } 132 }
131 133
134 # --- M11: render-vs-dump convergence -----------------------------------
135 # converged_quiet CLIENT_OUT SOCK — render the captured client stream and
136 # diff it against the daemon's grid, plain and styled. Nonzero on
137 # divergence, leaving CLIENT_OUT.{render,dump,diff,rvt,dvt} behind for
138 # inspection. CLIENT_OUT must be PURE client stdout: a capture taken with
139 # 2>&1 has exit messages and predict stats mixed into the escape stream.
140 converged_quiet() {
141 _co="$1"; _cs="$2"
142 "$RENDER" < "$_co" > "$_co.render" || return 1
143 "$MUXD" dump --sock "$_cs" > "$_co.dump" || return 1
144 # Trailing whitespace is a formatting difference between two correct
145 # grids (padded vs unpadded row ends), not a divergence.
146 sed 's/[[:space:]]*$//' "$_co.render" > "$_co.render.n"
147 sed 's/[[:space:]]*$//' "$_co.dump" > "$_co.dump.n"
148 diff -u "$_co.dump.n" "$_co.render.n" > "$_co.diff" || return 1
149 # Styled, byte-for-byte: both sides come out of the same formatter, so
150 # equal grids are equal bytes. This is the half that sees a bled SGR
151 # or a leftover prediction underline — plain text dumps the same glyph
152 # either way, which is exactly why plain alone cannot carry M9's
153 # overlay-never-becomes-state invariant.
154 "$RENDER" --vt < "$_co" > "$_co.rvt" || return 1
155 "$MUXD" dump --vt --sock "$_cs" > "$_co.dvt" || return 1
156 cmp -s "$_co.dvt" "$_co.rvt" || return 1
157 rm -f "$_co.render" "$_co.dump" "$_co.render.n" "$_co.dump.n" \
158 "$_co.diff" "$_co.rvt" "$_co.dvt"
159 return 0
160 }
161
162 # assert_converged CLIENT_OUT SOCK NAME — a scenario's LAST act, after
163 # quiesce and after the client detached: mid-scenario the dump is still
164 # moving, and another attach would claim the grid.
165 CONV_COUNT=0
166 assert_converged() {
167 CONV_COUNT=$((CONV_COUNT + 1))
168 converged_quiet "$1" "$2" || {
169 echo "e2e FAIL: $3: client render diverges from daemon grid (-daemon +client):"
170 head -40 "$1.diff" 2>/dev/null || \
171 echo "(no plain diff: divergence is styled-only, or the helper failed)"
172 echo "grids left in $1.render / $1.dump / $1.rvt / $1.dvt"
173 exit 1
174 }
175 }
176
132 cleanup() { 177 cleanup() {
133 # `${DPID:-}` rather than `$DPID`: DPID is the one variable this trap 178 # `${DPID:-}` rather than `$DPID`: DPID is the one variable this trap
134 # reads that is assigned AFTER the trap is installed, and the scenarios 179 # reads that is assigned AFTER the trap is installed, and the scenarios
@@ -160,7 +205,13 @@ cleanup() {
160 "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" \ 205 "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" \
161 "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \ 206 "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \
162 "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY.wrong" \ 207 "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY.wrong" \
163 "$OUT.nokey" "$SOCK4.nokey" 208 "$OUT.nokey" "$SOCK4.nokey" \
209 "$OUT.doctored" "$OUT.doctored.render" "$OUT.doctored.dump" \
210 "$OUT.doctored.render.n" "$OUT.doctored.dump.n" "$OUT.doctored.diff" \
211 "$OUT.doctored.rvt" "$OUT.doctored.dvt"
212 # The convergence files a FAILING assert_converged leaves behind
213 # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not
214 # chased here: on a failing run they are the evidence.
164 rm -rf "$XDG_CONFIG_HOME" "$XDG_STATE_HOME" "${NOKEY_CFG:-}" 215 rm -rf "$XDG_CONFIG_HOME" "$XDG_STATE_HOME" "${NOKEY_CFG:-}"
165 } 216 }
166 trap cleanup EXIT INT TERM 217 trap cleanup EXIT INT TERM
@@ -215,6 +266,22 @@ grep -q "e2e-works" "$OUT" || {
215 # 3. Detach left the daemon running. 266 # 3. Detach left the daemon running.
216 kill -0 "$DPID" || { echo "e2e FAIL: daemon died on detach"; exit 1; } 267 kill -0 "$DPID" || { echo "e2e FAIL: daemon died on detach"; exit 1; }
217 268
269 # 4. The M11 claim itself: the screen the client painted equals the
270 # screen the daemon holds — same engine, same formatter, both formats.
271 assert_converged "$OUT" "$SOCK" "base attach"
272
273 # The control: a doctored stream must NOT converge. A convergence check
274 # that cannot fail proves nothing (the wan.sh rule, M9).
275 cp "$OUT" "$OUT.doctored"
276 printf '\033[12;1Hconvergence-control-glyphs' >> "$OUT.doctored"
277 if converged_quiet "$OUT.doctored" "$SOCK"; then
278 echo "e2e FAIL: convergence control did not fire on a doctored stream"; exit 1
279 fi
280 rm -f "$OUT.doctored" "$OUT.doctored.render" "$OUT.doctored.dump" \
281 "$OUT.doctored.render.n" "$OUT.doctored.dump.n" "$OUT.doctored.diff" \
282 "$OUT.doctored.rvt" "$OUT.doctored.dvt"
283 echo "e2e OK: convergence control fires on a doctored stream"
284
218 # --- M3: kill a client mid-run; daemon survives; reattach lands correctly. 285 # --- M3: kill a client mid-run; daemon survives; reattach lands correctly.
219 { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" & 286 { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" &
220 CPID=$! 287 CPID=$!