479b5c2e
test: e2e - a rebooted daemon's panes wear gone and the cut never moves
a73x 2026-09-01 10:50
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -170,8 +170,8 @@ done | |||
| 170 | # one of those and adds a convergence point would be pinning a fact every | 170 | # one of those and adds a convergence point would be pinning a fact every |
| 171 | # leg above already establishes. | 171 | # leg above already establishes. |
| 172 | 172 | ||
| 173 | [ "$OK_COUNT" = "100" ] || { | 173 | [ "$OK_COUNT" = "101" ] || { |
| 174 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 100 —" | 174 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 101 —" |
| 175 | echo " a scenario was added (update the pin) or silently lost" | 175 | echo " a scenario was added (update the pin) or silently lost" |
| 176 | exit 1 | 176 | exit 1 |
| 177 | } | 177 | } |
test/e2e_12_panes.sh
| Old | New | ||
|---|---|---|---|
| @@ -35,6 +35,8 @@ SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock" | |||
| 35 | defer_sock "$SOCK60" | 35 | defer_sock "$SOCK60" |
| 36 | SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" | 36 | SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" |
| 37 | defer_sock "$SOCK61" | 37 | defer_sock "$SOCK61" |
| 38 | SOCK62="${TMPDIR:-/tmp}/muxd-e2e-gonepanes-$$.sock" | ||
| 39 | defer_sock "$SOCK62" | ||
| 38 | 40 | ||
| 39 | # --- the cursor rests in the focused tile, whoever painted last ---------- | 41 | # --- the cursor rests in the focused tile, whoever painted last ---------- |
| 40 | # The cursor-ownership leg: a daemon of its own, for the grid-size reason | 42 | # The cursor-ownership leg: a daemon of its own, for the grid-size reason |
| @@ -907,3 +909,190 @@ D62PID="" | |||
| 907 | rm -rf "$LPDSTATE" | 909 | rm -rf "$LPDSTATE" |
| 908 | ok "a corrupted sidecar degrades silently to the default layout" | 910 | ok "a corrupted sidecar degrades silently to the default layout" |
| 909 | 911 | ||
| 912 | # ---- gone panes: a rebooted daemon keeps the cut ---------------------- | ||
| 913 | # | ||
| 914 | # The spec's scenario, end to end: three sessions, a saved three-pane cut, | ||
| 915 | # and a daemon that comes back EMPTY. The wall paints the saved cut and | ||
| 916 | # never moves it — panes b and c dress `gone` where they stood instead of | ||
| 917 | # collapsing out, which is what the pre-gone-panes build did on the first | ||
| 918 | # poll answer. Enter in pane b creates session b anew in the same rect; a | ||
| 919 | # later run `x`es pane c, and only THAT collapses it. | ||
| 920 | # | ||
| 921 | # Three sessions and not two: the fates have to differ per pane (one binds | ||
| 922 | # live, one revives, one is dismissed), and a two-pane fixture could not | ||
| 923 | # tell "the cut stood" from "the cut happened to be the default". | ||
| 924 | GPSTATE="${TMPDIR:-/tmp}/mux-e2e-gonepanes-state-$$" | ||
| 925 | defer_rm "$GPSTATE" | ||
| 926 | GPHOSTS="$GPSTATE/mux/hosts" | ||
| 927 | GPLAYOUT="$GPSTATE/mux/layout" | ||
| 928 | start_daemon "$SOCK62" "$OUT.gp.d" "gone-panes daemon never bound" --shell /bin/sh | ||
| 929 | D63PID=$DPID | ||
| 930 | |||
| 931 | pipe_mux "$OUT.gpa" "$OUT.gpa.err" env XDG_STATE_HOME="$GPSTATE" timeout 40 "$MUX" --sock "$SOCK62" | ||
| 932 | pipe_send 'printf "gp-%%s\\n" zero\n' | ||
| 933 | await_out "$OUT.gpa" "gp-zero" "gone-panes: the default session's marker never reached the client" | ||
| 934 | pipe_detach | ||
| 935 | wait_grid "$SOCK62" "gp-zero" "gone-panes: the default session's marker" | ||
| 936 | pipe_mux "$OUT.gpb" "$OUT.gpb.err" env XDG_STATE_HOME="$GPSTATE" timeout 40 "$MUX" --sock "$SOCK62" --session b | ||
| 937 | pipe_send 'printf "gp-%%s\\n" bee\n' | ||
| 938 | await_out "$OUT.gpb" "gp-bee" "gone-panes: session b's marker never reached the client" | ||
| 939 | pipe_detach | ||
| 940 | wait_grid "$SOCK62" "gp-bee" "gone-panes: session b's marker" b | ||
| 941 | pipe_mux "$OUT.gpc" "$OUT.gpc.err" env XDG_STATE_HOME="$GPSTATE" timeout 40 "$MUX" --sock "$SOCK62" --session c | ||
| 942 | pipe_send 'printf "gp-%%s\\n" sea\n' | ||
| 943 | await_out "$OUT.gpc" "gp-sea" "gone-panes: session c's marker never reached the client" | ||
| 944 | pipe_detach | ||
| 945 | wait_grid "$SOCK62" "gp-sea" "gone-panes: session c's marker" c | ||
| 946 | |||
| 947 | # One line, the daemon. Its three sessions are what the poll turns into | ||
| 948 | # three panes; the wall knows nothing about them until it asks. | ||
| 949 | mkdir -p "$GPSTATE/mux" | ||
| 950 | printf -- '--sock %s\n' "$SOCK62" > "$GPHOSTS" | ||
| 951 | # No saved tree yet, for the restore leg's reason: what this leg saves and | ||
| 952 | # reads back has to be the tree run 1 builds. | ||
| 953 | no_saved_tree "$GPSTATE" | ||
| 954 | |||
| 955 | set +e | ||
| 956 | # Run 1: hydrate the three-pane wall on a tty and detach. The detach saves | ||
| 957 | # the sidecar, which is the cut every later run has to be measured against. | ||
| 958 | XDG_STATE_HOME="$GPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ | ||
| 959 | --out "$OUT.gpcap1" --err "$OUT.gpcap1.err" -- \ | ||
| 960 | "$MUX" > "$OUT.gppc1" 2>&1 <<'EOF' | ||
| 961 | expect gp-zero 20000 | ||
| 962 | settle 1200 25000 | ||
| 963 | send \x1cd | ||
| 964 | waitexit 10000 | ||
| 965 | EOF | ||
| 966 | RC=$? | ||
| 967 | set -e | ||
| 968 | [ "$RC" -eq 0 ] || { | ||
| 969 | echo "e2e FAIL: gone-panes: run 1's ptyclient exited $RC:" | ||
| 970 | cat "$OUT.gppc1" "$OUT.gpcap1.err"; exit 1; } | ||
| 971 | [ -f "$GPLAYOUT" ] || { | ||
| 972 | echo "e2e FAIL: gone-panes: sidecar not saved at $GPLAYOUT" | ||
| 973 | ls -la "$GPSTATE/mux/" 2>&1; exit 1; } | ||
| 974 | # Three leaves, and the ORDER matters below: a seeded pane takes the tile | ||
| 975 | # index of its leaf, so `Ctrl-\ 2` is leaf 1 and `Ctrl-\ 3` is leaf 2. Pin | ||
| 976 | # the mapping here rather than in a comment, or a change in the order the | ||
| 977 | # poll births tiles would silently retarget every chord this leg sends. | ||
| 978 | _gp_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ') | ||
| 979 | [ "$_gp_leaves" = "0 b c " ] || { | ||
| 980 | echo "e2e FAIL: gone-panes: sidecar leaves are '$_gp_leaves', want '0 b c ':" | ||
| 981 | cat "$GPLAYOUT"; exit 1; } | ||
| 982 | # The rails of the saved cut: a three-way `beside` at 80 columns has two. | ||
| 983 | # The glyph is matched WHOLE — CUP, reverse video, ONE space, reset — for | ||
| 984 | # the layout-restore leg's reason: a label bar opens with the same CUP+SGR | ||
| 985 | # at its pane's own left column, and a looser prefix counts bars as rails. | ||
| 986 | _gp_railre=$'\x1b\[[0-9]+;[0-9]+H\x1b\[7m \x1b\[0m' | ||
| 987 | _gp_rails1=$(grep -aoE "$_gp_railre" "$OUT.gpcap1" | | ||
| 988 | sed -E 's/.*;([0-9]+)H.*/\1/' | sort -n | uniq | tr '\n' ' ') | ||
| 989 | [ "$(printf '%s' "$_gp_rails1" | wc -w)" -eq 2 ] || { | ||
| 990 | echo "e2e FAIL: gone-panes: run 1 painted rails at '$_gp_rails1', want two columns" | ||
| 991 | echo " — the three-pane cut this leg measures never happened" | ||
| 992 | cat "$OUT.gppc1"; exit 1; } | ||
| 993 | |||
| 994 | # The reboot: the same socket, a fresh daemon, none of our sessions on it. | ||
| 995 | # A fresh daemon is born holding `0`, so pane 0 has a live session to bind | ||
| 996 | # and panes b and c have none — reachable host, missing sessions, which is | ||
| 997 | # exactly the `gone` case and not the `unreachable` one. | ||
| 998 | assert_stopped "$SOCK62" "$D63PID" "gone-panes" "$OUT.gpstop1" | ||
| 999 | start_daemon "$SOCK62" "$OUT.gp.d2" "gone-panes daemon 2 never bound" --shell /bin/sh | ||
| 1000 | D63PID=$DPID | ||
| 1001 | |||
| 1002 | set +e | ||
| 1003 | # Run 2: the saved cut must paint and STAY. No chord, no resize, nothing | ||
| 1004 | # but time — two and a half seconds of quiet is several poll answers, and | ||
| 1005 | # the first of them is what used to re-cut this wall. The marker at the end | ||
| 1006 | # proves the one pane that DID find its session is bound to a live shell. | ||
| 1007 | XDG_STATE_HOME="$GPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ | ||
| 1008 | --out "$OUT.gpcap2" --err "$OUT.gpcap2.err" -- \ | ||
| 1009 | "$MUX" > "$OUT.gppc2" 2>&1 <<'EOF' | ||
| 1010 | settle 2500 30000 | ||
| 1011 | send printf 'gp-%s\n' watch\n | ||
| 1012 | expect gp-watch 20000 | ||
| 1013 | settle 700 15000 | ||
| 1014 | send \x1cd | ||
| 1015 | waitexit 10000 | ||
| 1016 | EOF | ||
| 1017 | RC=$? | ||
| 1018 | set -e | ||
| 1019 | [ "$RC" -eq 0 ] || { | ||
| 1020 | echo "e2e FAIL: gone-panes: run 2's ptyclient exited $RC:" | ||
| 1021 | cat "$OUT.gppc2" "$OUT.gpcap2.err"; exit 1; } | ||
| 1022 | # The word reached the grid. A bar is byte-truncated from the LABEL end and | ||
| 1023 | # the state survives that cut (`labelText`), so `[gone]` is on screen for | ||
| 1024 | # any pane width this leg could produce. | ||
| 1025 | grep -qa '\[gone\]' "$OUT.gpcap2" || { | ||
| 1026 | echo "e2e FAIL: gone-panes: no pane wore [gone] after the reboot:" | ||
| 1027 | cat "$OUT.gppc2"; exit 1; } | ||
| 1028 | # The rails did not move. A capture is a stream and holds every rail the | ||
| 1029 | # run ever painted, so this catches a re-cut that MOVES a boundary; the | ||
| 1030 | # sidecar below is what catches one that removes a pane (measured against | ||
| 1031 | # the pre-gone-panes build 2026-09-01: it collapsed to a single leaf while | ||
| 1032 | # leaving both of the original rails in this capture). | ||
| 1033 | _gp_rails2=$(grep -aoE "$_gp_railre" "$OUT.gpcap2" | | ||
| 1034 | sed -E 's/.*;([0-9]+)H.*/\1/' | sort -n | uniq | tr '\n' ' ') | ||
| 1035 | [ "$_gp_rails1" = "$_gp_rails2" ] || { | ||
| 1036 | echo "e2e FAIL: gone-panes: rails moved across the reboot (run 1: $_gp_rails1 run 2: $_gp_rails2)" | ||
| 1037 | cat "$OUT.gppc2"; exit 1; } | ||
| 1038 | # ...and the structural half, which is the one the pre-gone-panes build | ||
| 1039 | # fails: run 2 detached from a wall of THREE panes, still named 0, b and c. | ||
| 1040 | # A build that collapses a disowned pane saves a sidecar of one leaf here. | ||
| 1041 | _gp_leaves2=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ') | ||
| 1042 | [ "$_gp_leaves2" = "0 b c " ] || { | ||
| 1043 | echo "e2e FAIL: gone-panes: after the reboot the saved cut is '$_gp_leaves2', want '0 b c ':" | ||
| 1044 | echo " — the panes whose sessions the daemon forgot were collapsed out" | ||
| 1045 | cat "$GPLAYOUT"; exit 1; } | ||
| 1046 | |||
| 1047 | set +e | ||
| 1048 | # Run 3: the two verbs a gone pane has. Enter in pane b (leaf 1) creates | ||
| 1049 | # session b anew in the same rect; `x` in pane c (leaf 2) drops that tile | ||
| 1050 | # on the KEYPRESS — one press, because a pane with no pump reads no ask and | ||
| 1051 | # `endKey` answers `.drop` rather than arming the two-step. | ||
| 1052 | XDG_STATE_HOME="$GPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \ | ||
| 1053 | --out "$OUT.gpcap3" --err "$OUT.gpcap3.err" -- \ | ||
| 1054 | "$MUX" > "$OUT.gppc3" 2>&1 <<'EOF' | ||
| 1055 | settle 2500 30000 | ||
| 1056 | send \x1c2 | ||
| 1057 | settle 700 15000 | ||
| 1058 | send \r | ||
| 1059 | settle 2000 25000 | ||
| 1060 | send printf 'gp-%s\n' reborn\n | ||
| 1061 | expect gp-reborn 20000 | ||
| 1062 | settle 700 15000 | ||
| 1063 | send \x1c3 | ||
| 1064 | settle 700 15000 | ||
| 1065 | send \x1cx | ||
| 1066 | settle 1500 20000 | ||
| 1067 | send \x1cd | ||
| 1068 | waitexit 10000 | ||
| 1069 | EOF | ||
| 1070 | RC=$? | ||
| 1071 | set -e | ||
| 1072 | [ "$RC" -eq 0 ] || { | ||
| 1073 | echo "e2e FAIL: gone-panes: run 3's ptyclient exited $RC:" | ||
| 1074 | cat "$OUT.gppc3" "$OUT.gpcap3.err"; exit 1; } | ||
| 1075 | # The revived session is REAL and carries the name its pane remembered: | ||
| 1076 | # the daemon's own grid for `b` holds the marker, and the entry session | ||
| 1077 | # does not — a revive that created `0` again, or created nothing and typed | ||
| 1078 | # into the focused survivor, fails one of these two. | ||
| 1079 | wait_sessions "$SOCK62" 2 "gone-panes: the daemon should hold 0 and the revived b" | ||
| 1080 | timeout 20 "$MUX" a capture --sock "$SOCK62" --session b > "$OUT.gprb" 2>&1 | ||
| 1081 | grep -q "gp-reborn" "$OUT.gprb" || { | ||
| 1082 | echo "e2e FAIL: gone-panes: the reborn marker is not in session b:" | ||
| 1083 | cat "$OUT.gprb"; exit 1; } | ||
| 1084 | timeout 20 "$MUX" a capture --sock "$SOCK62" > "$OUT.gpra" 2>&1 | ||
| 1085 | ! grep -q "gp-reborn" "$OUT.gpra" || { | ||
| 1086 | echo "e2e FAIL: gone-panes: the reborn marker landed in session 0 — Enter did not" | ||
| 1087 | echo " revive the pane it was pressed in:" | ||
| 1088 | cat "$OUT.gpra"; exit 1; } | ||
| 1089 | # `x` dismissed pane c and nothing else: two leaves, and c is not one of | ||
| 1090 | # them. The sidecar is written on DETACH, so this reads run 3's save. | ||
| 1091 | _gp_leaves3=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ') | ||
| 1092 | [ "$_gp_leaves3" = "0 b " ] || { | ||
| 1093 | echo "e2e FAIL: gone-panes: after x the saved cut is '$_gp_leaves3', want '0 b ':" | ||
| 1094 | cat "$GPLAYOUT"; exit 1; } | ||
| 1095 | assert_stopped "$SOCK62" "$D63PID" "gone-panes" "$OUT.gpstop2" | ||
| 1096 | D63PID="" | ||
| 1097 | rm -rf "$GPSTATE" | ||
| 1098 | ok "a rebooted daemon's panes wear gone and the cut never moves; Enter revives, x dismisses" | ||