a73x

818302fd

test: convergence on the prediction scenarios; stats reads move to split stderr

a73x   2026-08-09 17:31

Commit message
test: convergence on the prediction scenarios; stats reads move to split stderr

test/e2e.sh
Old New
@@ -203,6 +203,7 @@ cleanup() {
203 "$OUT.m7.err" "$OUT.m7b.err" "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err" \ 203 "$OUT.m7.err" "$OUT.m7b.err" "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err" \
204 "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \ 204 "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \
205 "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \ 205 "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \
206 "$OUT.p1.err" "$OUT.pb.err" "$OUT.pw.err" "$OUT.rw.err" "$OUT.pr.err" \
206 "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ 207 "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \
207 "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" \ 208 "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" \
208 "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \ 209 "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \
@@ -1175,7 +1176,7 @@ set +e
1175 { sleep 2; printf 'z'; sleep 4; printf '\034'; } | \ 1176 { sleep 2; printf 'z'; sleep 4; printf '\034'; } | \
1176 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \ 1177 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \
1177 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ 1178 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \
1178 > "$OUT.p1" 2>&1 & 1179 > "$OUT.p1" 2> "$OUT.p1.err" &
1179 P1PID=$! 1180 P1PID=$!
1180 set -e 1181 set -e
1181 # Snapshot the client output 450ms after the keystroke: well 1182 # Snapshot the client output 450ms after the keystroke: well
@@ -1187,7 +1188,9 @@ set +e
1187 wait "$P1PID" 1188 wait "$P1PID"
1188 RC=$? 1189 RC=$?
1189 set -e 1190 set -e
1190 [ "$RC" -eq 0 ] || { echo "e2e FAIL: line-mode prediction client exited $RC"; cat "$OUT.p1"; exit 1; } 1191 [ "$RC" -eq 0 ] || {
1192 echo "e2e FAIL: line-mode prediction client exited $RC"
1193 cat "$OUT.p1" "$OUT.p1.err" 2>/dev/null; exit 1; }
1191 1194
1192 # The underlined glyph, in the snapshot taken before the echo could arrive. 1195 # The underlined glyph, in the snapshot taken before the echo could arrive.
1193 # This is the whole claim of the milestone, as an effect rather than a 1196 # This is the whole claim of the milestone, as an effect rather than a
@@ -1204,15 +1207,16 @@ if grep -q "$(printf '\033\[2K\033\[0mz')" "$OUT.p1.early"; then
1204 echo " either DELAY_MS was ignored (default is 150) or delaypipe is not delaying" 1207 echo " either DELAY_MS was ignored (default is 150) or delaypipe is not delaying"
1205 exit 1 1208 exit 1
1206 fi 1209 fi
1207 want_stat "$OUT.p1" contradicted 0 "line mode" 1210 want_stat "$OUT.p1.err" contradicted 0 "line mode"
1208 CONF=$(predict_stat "$OUT.p1" confirmed) 1211 CONF=$(predict_stat "$OUT.p1.err" confirmed)
1209 [ -n "$CONF" ] && [ "$CONF" -ge 1 ] || { 1212 [ -n "$CONF" ] && [ "$CONF" -ge 1 ] || {
1210 echo "e2e FAIL: line mode: confirmed=$CONF, want >=1"; grep "^predict " "$OUT.p1"; exit 1; 1213 echo "e2e FAIL: line mode: confirmed=$CONF, want >=1"; grep "^predict " "$OUT.p1.err"; exit 1;
1211 } 1214 }
1212 # The session really received the keystroke: cat echoed it into the grid. 1215 # The session really received the keystroke: cat echoed it into the grid.
1213 "$MUXD" dump --sock "$SOCK5" | grep -q "z" || { 1216 "$MUXD" dump --sock "$SOCK5" | grep -q "z" || {
1214 echo "e2e FAIL: line mode: daemon grid never saw the keystroke"; exit 1; 1217 echo "e2e FAIL: line mode: daemon grid never saw the keystroke"; exit 1;
1215 } 1218 }
1219 assert_converged "$OUT.p1" "$SOCK5" "line-mode prediction"
1216 1220
1217 # 2. A burst outrunning the round trip. This is reconcile v2's reason to 1221 # 2. A burst outrunning the round trip. This is reconcile v2's reason to
1218 # exist: under the old rule every character after the first was judged by 1222 # exist: under the old rule every character after the first was judged by
@@ -1224,16 +1228,18 @@ set +e
1224 sleep 4; printf '\034'; } | \ 1228 sleep 4; printf '\034'; } | \
1225 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \ 1229 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \
1226 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ 1230 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \
1227 > "$OUT.pb" 2>&1 1231 > "$OUT.pb" 2> "$OUT.pb.err"
1228 RC=$? 1232 RC=$?
1229 set -e 1233 set -e
1230 [ "$RC" -eq 0 ] || { echo "e2e FAIL: burst client exited $RC"; cat "$OUT.pb"; exit 1; } 1234 [ "$RC" -eq 0 ] || {
1235 echo "e2e FAIL: burst client exited $RC"
1236 cat "$OUT.pb" "$OUT.pb.err" 2>/dev/null; exit 1; }
1231 1237
1232 # Five keystrokes typed at 200ms into a 600ms round trip: three are always 1238 # Five keystrokes typed at 200ms into a 600ms round trip: three are always
1233 # outstanding at once. `made` is asserted too, so two keystrokes arriving in 1239 # outstanding at once. `made` is asserted too, so two keystrokes arriving in
1234 # one read (which would be suppressed as a multi-byte chunk, not predicted) 1240 # one read (which would be suppressed as a multi-byte chunk, not predicted)
1235 # fails loudly instead of quietly weakening the test. 1241 # fails loudly instead of quietly weakening the test.
1236 want_stat "$OUT.pb" made 5 "burst" 1242 want_stat "$OUT.pb.err" made 5 "burst"
1237 # `confirmed` is a LIVENESS counter and the only timing-dependent one here: 1243 # `confirmed` is a LIVENESS counter and the only timing-dependent one here:
1238 # the fifth confirmation can still be in flight when the detach byte lands, 1244 # the fifth confirmation can still be in flight when the detach byte lands,
1239 # which is why it is a floor rather than an equality. Observed 1 run in 3 1245 # which is why it is a floor rather than an equality. Observed 1 run in 3
@@ -1244,12 +1250,13 @@ want_stat "$OUT.pb" made 5 "burst"
1244 # `contradicted` and `expired` stay EXACT. They are the correctness claims — 1250 # `contradicted` and `expired` stay EXACT. They are the correctness claims —
1245 # a prediction judged wrong, or one that aged out unjudged, is a defect 1251 # a prediction judged wrong, or one that aged out unjudged, is a defect
1246 # whenever it happens, and nothing about timing makes either acceptable. 1252 # whenever it happens, and nothing about timing makes either acceptable.
1247 want_stat_ge "$OUT.pb" confirmed 4 "burst" 1253 want_stat_ge "$OUT.pb.err" confirmed 4 "burst"
1248 want_stat "$OUT.pb" contradicted 0 "burst" 1254 want_stat "$OUT.pb.err" contradicted 0 "burst"
1249 want_stat "$OUT.pb" expired 0 "burst" 1255 want_stat "$OUT.pb.err" expired 0 "burst"
1250 "$MUXD" dump --sock "$SOCK5" | grep -q "burst" || { 1256 "$MUXD" dump --sock "$SOCK5" | grep -q "burst" || {
1251 echo "e2e FAIL: burst: daemon grid does not hold the typed text"; exit 1; 1257 echo "e2e FAIL: burst: daemon grid does not hold the typed text"; exit 1;
1252 } 1258 }
1259 assert_converged "$OUT.pb" "$SOCK5" "burst"
1253 1260
1254 kill "$D5PID" 2>/dev/null || true 1261 kill "$D5PID" 2>/dev/null || true
1255 D5PID="" 1262 D5PID=""
@@ -1284,19 +1291,23 @@ while [ ! -S "$SOCK6" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
1284 set +e 1291 set +e
1285 { sleep 2; for c in h u n t e r 2; do printf '%s' "$c"; sleep 0.2; done; \ 1292 { sleep 2; for c in h u n t e r 2; do printf '%s' "$c"; sleep 0.2; done; \
1286 printf '\n'; sleep 2; printf '\034'; } | \ 1293 printf '\n'; sleep 2; printf '\034'; } | \
1287 MUX_PREDICT_STATS=1 timeout 40 "$MUX" --sock "$SOCK6" > "$OUT.pw" 2>&1 1294 MUX_PREDICT_STATS=1 timeout 40 "$MUX" --sock "$SOCK6" > "$OUT.pw" 2> "$OUT.pw.err"
1288 RC=$? 1295 RC=$?
1289 set -e 1296 set -e
1290 [ "$RC" -eq 0 ] || { echo "e2e FAIL: password client exited $RC"; cat "$OUT.pw"; exit 1; } 1297 [ "$RC" -eq 0 ] || {
1298 echo "e2e FAIL: password client exited $RC"
1299 cat "$OUT.pw" "$OUT.pw.err" 2>/dev/null; exit 1; }
1291 1300
1292 # Nothing was shown, and nothing was even attempted: echo-off canonical is 1301 # Nothing was shown, and nothing was even attempted: echo-off canonical is
1293 # the tier where a prediction must never be MADE, not merely never painted, 1302 # the tier where a prediction must never be MADE, not merely never painted,
1294 # because a made prediction sits in a buffer the overlay paints from. 1303 # because a made prediction sits in a buffer the overlay paints from.
1295 want_stat "$OUT.pw" made 0 "password" 1304 want_stat "$OUT.pw.err" made 0 "password"
1296 want_stat "$OUT.pw" displayed 0 "password" 1305 want_stat "$OUT.pw.err" displayed 0 "password"
1297 # The effect, not the counter: the secret appears nowhere in the bytes the 1306 # The effect, not the counter: the secret appears nowhere in the bytes the
1298 # client wrote to the terminal. 1307 # client wrote — either stream. The stats line cannot carry a secret, but
1299 if grep -q "hunter2" "$OUT.pw"; then 1308 # checking both costs one word and the claim is about everything the client
1309 # emitted, not about the half of it that happens to be the screen.
1310 if grep -q "hunter2" "$OUT.pw" "$OUT.pw.err"; then
1300 echo "e2e FAIL: the password appears in the client's terminal output"; exit 1 1311 echo "e2e FAIL: the password appears in the client's terminal output"; exit 1
1301 fi 1312 fi
1302 # ...and the run was not vacuous: the shell really received all seven 1313 # ...and the run was not vacuous: the shell really received all seven
@@ -1306,6 +1317,7 @@ fi
1306 echo "e2e FAIL: the password never reached the shell; the absence above proves nothing" 1317 echo "e2e FAIL: the password never reached the shell; the absence above proves nothing"
1307 "$MUXD" dump --sock "$SOCK6"; exit 1; 1318 "$MUXD" dump --sock "$SOCK6"; exit 1;
1308 } 1319 }
1320 assert_converged "$OUT.pw" "$SOCK6" "password"
1309 1321
1310 kill "$D6PID" 2>/dev/null || true 1322 kill "$D6PID" 2>/dev/null || true
1311 D6PID="" 1323 D6PID=""
@@ -1322,35 +1334,41 @@ while [ ! -S "$SOCK7" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
1322 set +e 1334 set +e
1323 { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 0.5; done; \ 1335 { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 0.5; done; \
1324 printf '\000'; sleep 0.5; printf 'j'; sleep 2.5; printf '\034'; } | \ 1336 printf '\000'; sleep 0.5; printf 'j'; sleep 2.5; printf '\034'; } | \
1325 MUX_PREDICT_STATS=1 timeout 40 "$MUX" --sock "$SOCK7" > "$OUT.rw" 2>&1 1337 MUX_PREDICT_STATS=1 timeout 40 "$MUX" --sock "$SOCK7" > "$OUT.rw" 2> "$OUT.rw.err"
1326 RC=$? 1338 RC=$?
1327 set -e 1339 set -e
1328 [ "$RC" -eq 0 ] || { echo "e2e FAIL: rawmode client exited $RC"; cat "$OUT.rw"; exit 1; } 1340 [ "$RC" -eq 0 ] || {
1341 echo "e2e FAIL: rawmode client exited $RC"
1342 cat "$OUT.rw" "$OUT.rw.err" 2>/dev/null; exit 1; }
1329 1343
1330 # Four printable keystrokes made predictions; the 0x00 that switched modes 1344 # Four printable keystrokes made predictions; the 0x00 that switched modes
1331 # is not printable and was refused. 1345 # is not printable and was refused.
1332 want_stat "$OUT.rw" made 4 "raw mode" 1346 want_stat "$OUT.rw.err" made 4 "raw mode"
1333 want_stat "$OUT.rw" suppressed 1 "raw mode" 1347 want_stat "$OUT.rw.err" suppressed 1 "raw mode"
1334 # a and b are invisible while promotion is earned; c is the first painted, 1348 # a and b are invisible while promotion is earned; c is the first painted,
1335 # and j is painted because by then display had been earned. 1349 # and j is painted because by then display had been earned.
1336 want_stat "$OUT.rw" displayed 2 "raw mode" 1350 want_stat "$OUT.rw.err" displayed 2 "raw mode"
1337 want_stat "$OUT.rw" confirmed 3 "raw mode" 1351 want_stat "$OUT.rw.err" confirmed 3 "raw mode"
1338 # j was swallowed: no frame ever answered the cell it was drawn in, so the 1352 # j was swallowed: no frame ever answered the cell it was drawn in, so the
1339 # expiry bound retired it rather than leaving a phantom glyph on screen for 1353 # expiry bound retired it rather than leaving a phantom glyph on screen for
1340 # the rest of the session. 1354 # the rest of the session.
1341 want_stat "$OUT.rw" expired 1 "raw mode" 1355 want_stat "$OUT.rw.err" expired 1 "raw mode"
1342 want_stat "$OUT.rw" contradicted 1 "raw mode" 1356 want_stat "$OUT.rw.err" contradicted 1 "raw mode"
1343 want_stat "$OUT.rw" abandoned 1 "raw mode" 1357 want_stat "$OUT.rw.err" abandoned 1 "raw mode"
1344 1358
1345 # Every prediction accounted for: made = confirmed + abandoned + pending, 1359 # Every prediction accounted for: made = confirmed + abandoned + pending,
1346 # and the queue is empty at exit, so nothing survived unexplained. 1360 # and the queue is empty at exit, so nothing survived unexplained.
1347 RW_MADE=$(predict_stat "$OUT.rw" made) 1361 RW_MADE=$(predict_stat "$OUT.rw.err" made)
1348 RW_CONF=$(predict_stat "$OUT.rw" confirmed) 1362 RW_CONF=$(predict_stat "$OUT.rw.err" confirmed)
1349 RW_ABND=$(predict_stat "$OUT.rw" abandoned) 1363 RW_ABND=$(predict_stat "$OUT.rw.err" abandoned)
1350 [ "$((RW_MADE - RW_CONF - RW_ABND))" -eq 0 ] || { 1364 [ "$((RW_MADE - RW_CONF - RW_ABND))" -eq 0 ] || {
1351 echo "e2e FAIL: raw mode: $((RW_MADE - RW_CONF - RW_ABND)) predictions unaccounted for" 1365 echo "e2e FAIL: raw mode: $((RW_MADE - RW_CONF - RW_ABND)) predictions unaccounted for"
1352 grep "^predict " "$OUT.rw"; exit 1; 1366 grep "^predict " "$OUT.rw.err"; exit 1;
1353 } 1367 }
1368 # The counters say `j` expired; this says the screen agrees. An expiry that
1369 # retired the prediction in the book but left its underlined glyph painted is
1370 # exactly the phantom the styled half of this check exists to catch.
1371 assert_converged "$OUT.rw" "$SOCK7" "raw mode"
1354 1372
1355 kill "$D7PID" 2>/dev/null || true 1373 kill "$D7PID" 2>/dev/null || true
1356 D7PID="" 1374 D7PID=""
@@ -1368,7 +1386,7 @@ set +e
1368 { sleep 2; printf 'p'; sleep 5; printf 'q'; sleep 4; printf '\034'; } | \ 1386 { sleep 2; printf 'p'; sleep 5; printf 'q'; sleep 4; printf '\034'; } | \
1369 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 60 "$MUX" \ 1387 DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 60 "$MUX" \
1370 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ 1388 --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \
1371 > "$OUT.pr" 2>&1 & 1389 > "$OUT.pr" 2> "$OUT.pr.err" &
1372 PRPID=$! 1390 PRPID=$!
1373 set -e 1391 set -e
1374 # Kill the transport a tenth of a second after the keystroke, while its 1392 # Kill the transport a tenth of a second after the keystroke, while its
@@ -1377,36 +1395,42 @@ set -e
1377 # --via string, so a pattern kill would take out the client under test. 1395 # --via string, so a pattern kill would take out the client under test.
1378 sleep 2.1 1396 sleep 2.1
1379 PP=$(proxy_pid "$SOCK5") 1397 PP=$(proxy_pid "$SOCK5")
1380 [ -n "$PP" ] || { echo "e2e FAIL: no proxy to tear down"; cat "$OUT.pr"; exit 1; } 1398 [ -n "$PP" ] || {
1399 echo "e2e FAIL: no proxy to tear down"
1400 cat "$OUT.pr" "$OUT.pr.err" 2>/dev/null; exit 1; }
1381 kill -9 "$PP" 1401 kill -9 "$PP"
1382 1402
1383 set +e 1403 set +e
1384 wait "$PRPID" 1404 wait "$PRPID"
1385 RC=$? 1405 RC=$?
1386 set -e 1406 set -e
1387 [ "$RC" -eq 0 ] || { echo "e2e FAIL: reconnect client exited $RC"; cat "$OUT.pr"; exit 1; } 1407 [ "$RC" -eq 0 ] || {
1408 echo "e2e FAIL: reconnect client exited $RC"
1409 cat "$OUT.pr" "$OUT.pr.err" 2>/dev/null; exit 1; }
1388 1410
1389 # The queue is empty at exit, by attribution rather than by inspection: 1411 # The queue is empty at exit, by attribution rather than by inspection:
1390 # nothing predicted is still outstanding, and the flush that dropped the 1412 # nothing predicted is still outstanding, and the flush that dropped the
1391 # in-flight prediction was not counted as anybody being wrong. 1413 # in-flight prediction was not counted as anybody being wrong.
1392 PR_MADE=$(predict_stat "$OUT.pr" made) 1414 PR_MADE=$(predict_stat "$OUT.pr.err" made)
1393 PR_CONF=$(predict_stat "$OUT.pr" confirmed) 1415 PR_CONF=$(predict_stat "$OUT.pr.err" confirmed)
1394 PR_ABND=$(predict_stat "$OUT.pr" abandoned) 1416 PR_ABND=$(predict_stat "$OUT.pr.err" abandoned)
1395 [ -n "$PR_MADE" ] || { echo "e2e FAIL: reconnect: no predict stats"; cat "$OUT.pr"; exit 1; } 1417 [ -n "$PR_MADE" ] || {
1418 echo "e2e FAIL: reconnect: no predict stats"
1419 cat "$OUT.pr" "$OUT.pr.err" 2>/dev/null; exit 1; }
1396 [ "$((PR_MADE - PR_CONF - PR_ABND))" -eq 0 ] || { 1420 [ "$((PR_MADE - PR_CONF - PR_ABND))" -eq 0 ] || {
1397 echo "e2e FAIL: reconnect: $((PR_MADE - PR_CONF - PR_ABND)) predictions outlived the tear" 1421 echo "e2e FAIL: reconnect: $((PR_MADE - PR_CONF - PR_ABND)) predictions outlived the tear"
1398 grep "^predict " "$OUT.pr"; exit 1; 1422 grep "^predict " "$OUT.pr.err"; exit 1;
1399 } 1423 }
1400 # And it was the reconnect that dropped it, not the expiry bound catching it 1424 # And it was the reconnect that dropped it, not the expiry bound catching it
1401 # a second later. Without this the scenario cannot tell the two apart: the 1425 # a second later. Without this the scenario cannot tell the two apart: the
1402 # resync after a reconnect flushes as well, so the outcome looks identical 1426 # resync after a reconnect flushes as well, so the outcome looks identical
1403 # either way and removing the reconnect's own flush passes unnoticed. It 1427 # either way and removing the reconnect's own flush passes unnoticed. It
1404 # did, until this line. 1428 # did, until this line.
1405 want_stat "$OUT.pr" expired 0 "reconnect" 1429 want_stat "$OUT.pr.err" expired 0 "reconnect"
1406 [ -n "$PR_ABND" ] && [ "$PR_ABND" -ge 1 ] || { 1430 [ -n "$PR_ABND" ] && [ "$PR_ABND" -ge 1 ] || {
1407 echo "e2e FAIL: reconnect: nothing was abandoned ($PR_ABND), so no prediction was" 1431 echo "e2e FAIL: reconnect: nothing was abandoned ($PR_ABND), so no prediction was"
1408 echo " outstanding when the transport died and the tear proves nothing" 1432 echo " outstanding when the transport died and the tear proves nothing"
1409 grep "^predict " "$OUT.pr"; exit 1; 1433 grep "^predict " "$OUT.pr.err"; exit 1;
1410 } 1434 }
1411 # The session survived and the client resumed into it: the keystroke typed 1435 # The session survived and the client resumed into it: the keystroke typed
1412 # after the tear reached the shell. 1436 # after the tear reached the shell.
@@ -1414,6 +1438,7 @@ want_stat "$OUT.pr" expired 0 "reconnect"
1414 echo "e2e FAIL: reconnect: the post-tear keystroke never reached the session" 1438 echo "e2e FAIL: reconnect: the post-tear keystroke never reached the session"
1415 "$MUXD" dump --sock "$SOCK5"; exit 1; 1439 "$MUXD" dump --sock "$SOCK5"; exit 1;
1416 } 1440 }
1441 assert_converged "$OUT.pr" "$SOCK5" "reconnect flush"
1417 1442
1418 kill "$D5PID" 2>/dev/null || true 1443 kill "$D5PID" 2>/dev/null || true
1419 D5PID="" 1444 D5PID=""