af1908af
test: e2e — prediction confirmed, suppressed, demoted, and flushed, by counter
a73x 2026-08-08 16:31
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -248,6 +248,11 @@ pub fn build(b: *std.Build) void { | |||
| 248 | const e2e = b.addSystemCommand(&.{"test/e2e.sh"}); | 248 | const e2e = b.addSystemCommand(&.{"test/e2e.sh"}); |
| 249 | e2e.addArtifactArg(exe); | 249 | e2e.addArtifactArg(exe); |
| 250 | e2e.addArtifactArg(mux_exe); | 250 | e2e.addArtifactArg(mux_exe); |
| 251 | // The prediction scenarios need a deterministic editor and a slow path; | ||
| 252 | // passed as artifacts so the suite runs against the binaries this build | ||
| 253 | // just produced rather than whatever is installed. | ||
| 254 | e2e.addArtifactArg(rawmode_exe); | ||
| 255 | e2e.addArtifactArg(delaypipe_exe); | ||
| 251 | const e2e_step = b.step("e2e", "Run end-to-end test"); | 256 | const e2e_step = b.step("e2e", "Run end-to-end test"); |
| 252 | e2e_step.dependOn(&e2e.step); | 257 | e2e_step.dependOn(&e2e.step); |
| 253 | 258 | ||
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -4,6 +4,10 @@ | |||
| 4 | set -eu | 4 | set -eu |
| 5 | MUXD="$1" | 5 | MUXD="$1" |
| 6 | MUX="$2" | 6 | MUX="$2" |
| 7 | # M9 prediction helpers: a deterministic stand-in for an editor, and a pipe | ||
| 8 | # that makes a slow round trip without netem or root. | ||
| 9 | RAWMODE="$3" | ||
| 10 | DELAYPIPE="$4" | ||
| 7 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" | 11 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" |
| 8 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" | 12 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" |
| 9 | # Second daemon, used only by the M7 abort scenario; declared here so the | 13 | # Second daemon, used only by the M7 abort scenario; declared here so the |
| @@ -22,6 +26,37 @@ D4PID="" | |||
| 22 | # A port out of the way of the ephemeral range, made per-run so two suites can | 26 | # A port out of the way of the ephemeral range, made per-run so two suites can |
| 23 | # overlap. Collisions surface as a loud bind failure, never as a silent pass. | 27 | # overlap. Collisions surface as a loud bind failure, never as a silent pass. |
| 24 | QPORT=$(( 21000 + ($$ % 4000) )) | 28 | QPORT=$(( 21000 + ($$ % 4000) )) |
| 29 | # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it | ||
| 30 | # controls, so they cannot share the long-lived /bin/sh daemon. | ||
| 31 | SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock" | ||
| 32 | D5PID="" | ||
| 33 | SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock" | ||
| 34 | D6PID="" | ||
| 35 | SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock" | ||
| 36 | D7PID="" | ||
| 37 | PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh" | ||
| 38 | |||
| 39 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | ||
| 40 | # such line on exit; every field is a key=value pair, so a rename or reorder | ||
| 41 | # in the client shows up here as an empty read rather than a wrong number. | ||
| 42 | predict_stat() { | ||
| 43 | sed -n "s/.*predict .*$2=\([0-9]*\).*/\1/p" "$1" | head -1 | ||
| 44 | } | ||
| 45 | |||
| 46 | # Assert one counter, with the whole line in the failure so a wrong number is | ||
| 47 | # read in context rather than alone. | ||
| 48 | want_stat() { | ||
| 49 | _got=$(predict_stat "$1" "$2") | ||
| 50 | [ -n "$_got" ] || { | ||
| 51 | echo "e2e FAIL: $4: no predict stats line (wanted $2=$3); got:" | ||
| 52 | cat "$1"; exit 1; | ||
| 53 | } | ||
| 54 | [ "$_got" = "$3" ] || { | ||
| 55 | echo "e2e FAIL: $4: $2=$_got, want $3" | ||
| 56 | grep "^predict " "$1" || true | ||
| 57 | exit 1; | ||
| 58 | } | ||
| 59 | } | ||
| 25 | 60 | ||
| 26 | # Wait until PATTERN shows up in FILE (default 15s). Timing that keys off the | 61 | # Wait until PATTERN shows up in FILE (default 15s). Timing that keys off the |
| 27 | # session's own output instead of a fixed sleep: the marker is proof the | 62 | # session's own output instead of a fixed sleep: the marker is proof the |
| @@ -51,7 +86,12 @@ cleanup() { | |||
| 51 | [ -n "$D2PID" ] && kill "$D2PID" 2>/dev/null || true | 86 | [ -n "$D2PID" ] && kill "$D2PID" 2>/dev/null || true |
| 52 | [ -n "$D3PID" ] && kill "$D3PID" 2>/dev/null || true | 87 | [ -n "$D3PID" ] && kill "$D3PID" 2>/dev/null || true |
| 53 | [ -n "$D4PID" ] && kill "$D4PID" 2>/dev/null || true | 88 | [ -n "$D4PID" ] && kill "$D4PID" 2>/dev/null || true |
| 54 | rm -f "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ | 89 | [ -n "$D5PID" ] && kill "$D5PID" 2>/dev/null || true |
| 90 | [ -n "$D6PID" ] && kill "$D6PID" 2>/dev/null || true | ||
| 91 | [ -n "$D7PID" ] && kill "$D7PID" 2>/dev/null || true | ||
| 92 | rm -f "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \ | ||
| 93 | "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \ | ||
| 94 | "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ | ||
| 55 | "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" \ | 95 | "$OUT" "$OUT.kill" "$OUT.re" "$OUT.a" \ |
| 56 | "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \ | 96 | "$OUT.b" "$OUT.via" "$OUT.dead" "$OUT.abort" "$OUT.m7" "$OUT.m7b" \ |
| 57 | "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY.wrong" | 97 | "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY.wrong" |
| @@ -633,4 +673,269 @@ kill "$D4PID" 2>/dev/null || true | |||
| 633 | D4PID="" | 673 | D4PID="" |
| 634 | rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" | 674 | rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" |
| 635 | 675 | ||
| 676 | # ---- M9: prediction --------------------------------------------------- | ||
| 677 | # | ||
| 678 | # Every scenario here asserts COUNTERS, because the screen cannot tell a | ||
| 679 | # predicted glyph from an echoed one — that is the entire point of the | ||
| 680 | # feature — so a rendering test would pass just as happily with prediction | ||
| 681 | # switched off. | ||
| 682 | # | ||
| 683 | # The session shell is /bin/cat, not /bin/sh. An interactive bash prompt is | ||
| 684 | # icanon=0/echo=0 (readline echoes for itself), which is the ADAPTIVE tier, | ||
| 685 | # not the always-predict one; cat is a genuinely canonical reader and the | ||
| 686 | # only way to exercise `.always` here. That distinction cost a milestone's | ||
| 687 | # worth of confusion to learn and is why it is written down twice. | ||
| 688 | |||
| 689 | # Delay per direction. The round trip is twice this, and every assertion | ||
| 690 | # below about "before the daemon could have answered" is measured against | ||
| 691 | # it. Deliberately larger than the plan's 150ms: the margin between "the | ||
| 692 | # prediction is painted" and "the echo could have arrived" is what keeps | ||
| 693 | # this scenario from being a race, and 300ms each way makes that margin | ||
| 694 | # half a second rather than a tenth. | ||
| 695 | PDELAY=300 | ||
| 696 | |||
| 697 | "$MUXD" run --sock "$SOCK5" --shell /bin/cat > "$OUT.p1" 2>&1 & | ||
| 698 | D5PID=$! | ||
| 699 | i=0 | ||
| 700 | while [ ! -S "$SOCK5" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done | ||
| 701 | [ -S "$SOCK5" ] || { echo "e2e FAIL: prediction daemon never bound"; cat "$OUT.p1"; exit 1; } | ||
| 702 | |||
| 703 | # 1. Line mode: the glyph is on screen before the round trip could have | ||
| 704 | # delivered it. | ||
| 705 | set +e | ||
| 706 | { sleep 2; printf 'z'; sleep 4; printf '\034'; } | \ | ||
| 707 | DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \ | ||
| 708 | --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ | ||
| 709 | > "$OUT.p1" 2>&1 & | ||
| 710 | P1PID=$! | ||
| 711 | set -e | ||
| 712 | # Snapshot the client's output a quarter second after the keystroke — long | ||
| 713 | # before the 600ms round trip could bring the pty's own echo back. | ||
| 714 | sleep 2.25 | ||
| 715 | cp "$OUT.p1" "$OUT.p1.early" 2>/dev/null || true | ||
| 716 | set +e | ||
| 717 | wait "$P1PID" | ||
| 718 | RC=$? | ||
| 719 | set -e | ||
| 720 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: line-mode prediction client exited $RC"; cat "$OUT.p1"; exit 1; } | ||
| 721 | |||
| 722 | # The underlined glyph, in the snapshot taken before the echo could arrive. | ||
| 723 | # This is the whole claim of the milestone, as an effect rather than a | ||
| 724 | # counter: the character was on the screen while it was still in flight. | ||
| 725 | grep -q "$(printf '\033\[4mz')" "$OUT.p1.early" || { | ||
| 726 | echo "e2e FAIL: no predicted glyph 250ms after the keystroke (RTT is $((PDELAY * 2))ms)" | ||
| 727 | echo "--- early snapshot ---"; cat -v "$OUT.p1.early"; exit 1; | ||
| 728 | } | ||
| 729 | # ...and the daemon's own answer was NOT there yet, which is what makes the | ||
| 730 | # line above mean anything. A delta paints a row with EL(2) before its | ||
| 731 | # content; the prediction never does. | ||
| 732 | if grep -q "$(printf '\033\[2K\033\[0mz')" "$OUT.p1.early"; then | ||
| 733 | echo "e2e FAIL: the pty's echo arrived within 250ms; the delay pipe is not delaying," | ||
| 734 | echo " so the assertion above proves nothing about prediction" | ||
| 735 | exit 1 | ||
| 736 | fi | ||
| 737 | want_stat "$OUT.p1" contradicted 0 "line mode" | ||
| 738 | CONF=$(predict_stat "$OUT.p1" confirmed) | ||
| 739 | [ -n "$CONF" ] && [ "$CONF" -ge 1 ] || { | ||
| 740 | echo "e2e FAIL: line mode: confirmed=$CONF, want >=1"; grep "^predict " "$OUT.p1"; exit 1; | ||
| 741 | } | ||
| 742 | # The session really received the keystroke: cat echoed it into the grid. | ||
| 743 | "$MUXD" dump --sock "$SOCK5" | grep -q "z" || { | ||
| 744 | echo "e2e FAIL: line mode: daemon grid never saw the keystroke"; exit 1; | ||
| 745 | } | ||
| 746 | |||
| 747 | # 2. A burst outrunning the round trip. This is reconcile v2's reason to | ||
| 748 | # exist: under the old rule every character after the first was judged by | ||
| 749 | # a frame built before it was typed, read as a contradiction, and the | ||
| 750 | # whole queue was flushed once per round trip. If anyone regresses the | ||
| 751 | # three-way judgment, this scenario is what says so. | ||
| 752 | set +e | ||
| 753 | { sleep 2; for c in b u r s t; do printf '%s' "$c"; sleep 0.2; done; \ | ||
| 754 | sleep 4; printf '\034'; } | \ | ||
| 755 | DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 40 "$MUX" \ | ||
| 756 | --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ | ||
| 757 | > "$OUT.pb" 2>&1 | ||
| 758 | RC=$? | ||
| 759 | set -e | ||
| 760 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: burst client exited $RC"; cat "$OUT.pb"; exit 1; } | ||
| 761 | |||
| 762 | # Five keystrokes typed at 200ms into a 600ms round trip: three are always | ||
| 763 | # outstanding at once. `made` is asserted too, so two keystrokes arriving in | ||
| 764 | # one read (which would be suppressed as a multi-byte chunk, not predicted) | ||
| 765 | # fails loudly instead of quietly weakening the test. | ||
| 766 | want_stat "$OUT.pb" made 5 "burst" | ||
| 767 | want_stat "$OUT.pb" confirmed 5 "burst" | ||
| 768 | want_stat "$OUT.pb" contradicted 0 "burst" | ||
| 769 | want_stat "$OUT.pb" expired 0 "burst" | ||
| 770 | "$MUXD" dump --sock "$SOCK5" | grep -q "burst" || { | ||
| 771 | echo "e2e FAIL: burst: daemon grid does not hold the typed text"; exit 1; | ||
| 772 | } | ||
| 773 | |||
| 774 | kill "$D5PID" 2>/dev/null || true | ||
| 775 | D5PID="" | ||
| 776 | |||
| 777 | # 3. A password prompt. The session is canonical with echo OFF from the | ||
| 778 | # moment it starts, so the whole run sits in the tier that predicts | ||
| 779 | # nothing at all. | ||
| 780 | cat > "$PWSH" <<'PWEOF' | ||
| 781 | #!/bin/sh | ||
| 782 | stty -echo | ||
| 783 | printf 'pw-ready\n' | ||
| 784 | read secret | ||
| 785 | printf 'pw-len-%s\n' "${#secret}" | ||
| 786 | # Blocks forever rather than exiting: the shell exiting would end the | ||
| 787 | # session, and the daemon would be gone before its grid could be read. | ||
| 788 | read _hold | ||
| 789 | PWEOF | ||
| 790 | chmod +x "$PWSH" | ||
| 791 | |||
| 792 | "$MUXD" run --sock "$SOCK6" --shell "$PWSH" > "$OUT.pw" 2>&1 & | ||
| 793 | D6PID=$! | ||
| 794 | i=0 | ||
| 795 | while [ ! -S "$SOCK6" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done | ||
| 796 | [ -S "$SOCK6" ] || { echo "e2e FAIL: password daemon never bound"; cat "$OUT.pw"; exit 1; } | ||
| 797 | |||
| 798 | # One character per write, and NOT as a single `printf hunter2`. A whole | ||
| 799 | # word in one write reaches the client as a multi-byte chunk, which is | ||
| 800 | # refused for being multi-byte before the tier is ever consulted — so the | ||
| 801 | # scenario would pass with the password tier completely broken. It did: | ||
| 802 | # mapping echo-off canonical to the always-predict tier survived this | ||
| 803 | # scenario until the typing was split up. | ||
| 804 | set +e | ||
| 805 | { sleep 2; for c in h u n t e r 2; do printf '%s' "$c"; sleep 0.2; done; \ | ||
| 806 | printf '\n'; sleep 2; printf '\034'; } | \ | ||
| 807 | MUX_PREDICT_STATS=1 timeout 40 "$MUX" --sock "$SOCK6" > "$OUT.pw" 2>&1 | ||
| 808 | RC=$? | ||
| 809 | set -e | ||
| 810 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: password client exited $RC"; cat "$OUT.pw"; exit 1; } | ||
| 811 | |||
| 812 | # Nothing was shown, and nothing was even attempted: echo-off canonical is | ||
| 813 | # the tier where a prediction must never be MADE, not merely never painted, | ||
| 814 | # because a made prediction sits in a buffer the overlay paints from. | ||
| 815 | want_stat "$OUT.pw" made 0 "password" | ||
| 816 | want_stat "$OUT.pw" displayed 0 "password" | ||
| 817 | # The effect, not the counter: the secret appears nowhere in the bytes the | ||
| 818 | # client wrote to the terminal. | ||
| 819 | if grep -q "hunter2" "$OUT.pw"; then | ||
| 820 | echo "e2e FAIL: the password appears in the client's terminal output"; exit 1 | ||
| 821 | fi | ||
| 822 | # ...and the run was not vacuous: the shell really received all seven | ||
| 823 | # characters, so the absence above is prediction declining rather than | ||
| 824 | # nothing having been typed. | ||
| 825 | "$MUXD" dump --sock "$SOCK6" | grep -q "pw-len-7" || { | ||
| 826 | echo "e2e FAIL: the password never reached the shell; the absence above proves nothing" | ||
| 827 | "$MUXD" dump --sock "$SOCK6"; exit 1; | ||
| 828 | } | ||
| 829 | |||
| 830 | kill "$D6PID" 2>/dev/null || true | ||
| 831 | D6PID="" | ||
| 832 | |||
| 833 | # 4. Raw mode: display is earned, then lost to a keystroke the application | ||
| 834 | # swallows. rawmode echoes like an editor in insert mode until it is sent | ||
| 835 | # 0x00, after which it consumes input and prints nothing. | ||
| 836 | "$MUXD" run --sock "$SOCK7" --shell "$RAWMODE" > "$OUT.rw" 2>&1 & | ||
| 837 | D7PID=$! | ||
| 838 | i=0 | ||
| 839 | while [ ! -S "$SOCK7" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done | ||
| 840 | [ -S "$SOCK7" ] || { echo "e2e FAIL: rawmode daemon never bound"; cat "$OUT.rw"; exit 1; } | ||
| 841 | |||
| 842 | set +e | ||
| 843 | { sleep 2; for c in a b c; do printf '%s' "$c"; sleep 0.5; done; \ | ||
| 844 | printf '\000'; sleep 0.5; printf 'j'; sleep 2.5; printf '\034'; } | \ | ||
| 845 | MUX_PREDICT_STATS=1 timeout 40 "$MUX" --sock "$SOCK7" > "$OUT.rw" 2>&1 | ||
| 846 | RC=$? | ||
| 847 | set -e | ||
| 848 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: rawmode client exited $RC"; cat "$OUT.rw"; exit 1; } | ||
| 849 | |||
| 850 | # Four printable keystrokes made predictions; the 0x00 that switched modes | ||
| 851 | # is not printable and was refused. | ||
| 852 | want_stat "$OUT.rw" made 4 "raw mode" | ||
| 853 | want_stat "$OUT.rw" suppressed 1 "raw mode" | ||
| 854 | # a and b are invisible while promotion is earned; c is the first painted, | ||
| 855 | # and j is painted because by then display had been earned. | ||
| 856 | want_stat "$OUT.rw" displayed 2 "raw mode" | ||
| 857 | want_stat "$OUT.rw" confirmed 3 "raw mode" | ||
| 858 | # j was swallowed: no frame ever answered the cell it was drawn in, so the | ||
| 859 | # expiry bound retired it rather than leaving a phantom glyph on screen for | ||
| 860 | # the rest of the session. | ||
| 861 | want_stat "$OUT.rw" expired 1 "raw mode" | ||
| 862 | want_stat "$OUT.rw" contradicted 1 "raw mode" | ||
| 863 | want_stat "$OUT.rw" abandoned 1 "raw mode" | ||
| 864 | |||
| 865 | # Every prediction accounted for: made = confirmed + abandoned + pending, | ||
| 866 | # and the queue is empty at exit, so nothing survived unexplained. | ||
| 867 | RW_MADE=$(predict_stat "$OUT.rw" made) | ||
| 868 | RW_CONF=$(predict_stat "$OUT.rw" confirmed) | ||
| 869 | RW_ABND=$(predict_stat "$OUT.rw" abandoned) | ||
| 870 | [ "$((RW_MADE - RW_CONF - RW_ABND))" -eq 0 ] || { | ||
| 871 | echo "e2e FAIL: raw mode: $((RW_MADE - RW_CONF - RW_ABND)) predictions unaccounted for" | ||
| 872 | grep "^predict " "$OUT.rw"; exit 1; | ||
| 873 | } | ||
| 874 | |||
| 875 | kill "$D7PID" 2>/dev/null || true | ||
| 876 | D7PID="" | ||
| 877 | |||
| 878 | # 5. A transport torn down with predictions outstanding. The overlay must | ||
| 879 | # come back empty: what was queued was predicted against a connection | ||
| 880 | # that no longer exists. | ||
| 881 | "$MUXD" run --sock "$SOCK5" --shell /bin/cat > "$OUT.pr" 2>&1 & | ||
| 882 | D5PID=$! | ||
| 883 | i=0 | ||
| 884 | while [ ! -S "$SOCK5" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done | ||
| 885 | [ -S "$SOCK5" ] || { echo "e2e FAIL: reconnect daemon never bound"; cat "$OUT.pr"; exit 1; } | ||
| 886 | |||
| 887 | set +e | ||
| 888 | { sleep 2; printf 'p'; sleep 5; printf 'q'; sleep 4; printf '\034'; } | \ | ||
| 889 | DELAY_MS="$PDELAY" MUX_PREDICT_STATS=1 timeout 60 "$MUX" \ | ||
| 890 | --via "$DELAYPIPE | $MUXD proxy --sock $SOCK5 | $DELAYPIPE" \ | ||
| 891 | > "$OUT.pr" 2>&1 & | ||
| 892 | PRPID=$! | ||
| 893 | set -e | ||
| 894 | # Kill the transport a tenth of a second after the keystroke, while its | ||
| 895 | # prediction is still outstanding — the round trip is 600ms, so it cannot | ||
| 896 | # have been judged yet. By comm+pid: the client's own argv contains the | ||
| 897 | # --via string, so a pattern kill would take out the client under test. | ||
| 898 | sleep 2.1 | ||
| 899 | PP=$(proxy_pid "$SOCK5") | ||
| 900 | [ -n "$PP" ] || { echo "e2e FAIL: no proxy to tear down"; cat "$OUT.pr"; exit 1; } | ||
| 901 | kill -9 "$PP" | ||
| 902 | |||
| 903 | set +e | ||
| 904 | wait "$PRPID" | ||
| 905 | RC=$? | ||
| 906 | set -e | ||
| 907 | [ "$RC" -eq 0 ] || { echo "e2e FAIL: reconnect client exited $RC"; cat "$OUT.pr"; exit 1; } | ||
| 908 | |||
| 909 | # The queue is empty at exit, by attribution rather than by inspection: | ||
| 910 | # nothing predicted is still outstanding, and the flush that dropped the | ||
| 911 | # in-flight prediction was not counted as anybody being wrong. | ||
| 912 | PR_MADE=$(predict_stat "$OUT.pr" made) | ||
| 913 | PR_CONF=$(predict_stat "$OUT.pr" confirmed) | ||
| 914 | PR_ABND=$(predict_stat "$OUT.pr" abandoned) | ||
| 915 | [ -n "$PR_MADE" ] || { echo "e2e FAIL: reconnect: no predict stats"; cat "$OUT.pr"; exit 1; } | ||
| 916 | [ "$((PR_MADE - PR_CONF - PR_ABND))" -eq 0 ] || { | ||
| 917 | echo "e2e FAIL: reconnect: $((PR_MADE - PR_CONF - PR_ABND)) predictions outlived the tear" | ||
| 918 | grep "^predict " "$OUT.pr"; exit 1; | ||
| 919 | } | ||
| 920 | # And it was the reconnect that dropped it, not the expiry bound catching it | ||
| 921 | # a second later. Without this the scenario cannot tell the two apart: the | ||
| 922 | # resync after a reconnect flushes as well, so the outcome looks identical | ||
| 923 | # either way and removing the reconnect's own flush passes unnoticed. It | ||
| 924 | # did, until this line. | ||
| 925 | want_stat "$OUT.pr" expired 0 "reconnect" | ||
| 926 | [ -n "$PR_ABND" ] && [ "$PR_ABND" -ge 1 ] || { | ||
| 927 | echo "e2e FAIL: reconnect: nothing was abandoned ($PR_ABND), so no prediction was" | ||
| 928 | echo " outstanding when the transport died and the tear proves nothing" | ||
| 929 | grep "^predict " "$OUT.pr"; exit 1; | ||
| 930 | } | ||
| 931 | # The session survived and the client resumed into it: the keystroke typed | ||
| 932 | # after the tear reached the shell. | ||
| 933 | "$MUXD" dump --sock "$SOCK5" | grep -q "q" || { | ||
| 934 | echo "e2e FAIL: reconnect: the post-tear keystroke never reached the session" | ||
| 935 | "$MUXD" dump --sock "$SOCK5"; exit 1; | ||
| 936 | } | ||
| 937 | |||
| 938 | kill "$D5PID" 2>/dev/null || true | ||
| 939 | D5PID="" | ||
| 940 | |||
| 636 | echo "e2e OK" | 941 | echo "e2e OK" |