a73x

97a6a21f

test: four more Linux-only assumptions in the e2e harness

a73x   2026-09-04 10:16

Commit message
test: four more Linux-only assumptions in the e2e harness

A backtick in an unquoted heredoc is a command substitution wherever it
appears, comments included, and two ptyclient scripts in the agent group
quoted `x`, `d`, `expect` and `settle` that way. On Linux all four are
"command not found" and the run carries on. On macOS /usr/bin/expect
exists, so the shell started it, it read the rest of the heredoc as its own
script, and the group hung for its whole 600 s budget. The rule is stated
above each of those heredocs now.

`pid_args` answered a different SHAPE on each OS: Linux reads a
NUL-terminated /proc entry and left a trailing space that Darwin's
`ps -o args=` does not. The one caller comparing a whole argv with `=` had
to spell that space, and could only have learned it from Linux. The Linux
arm trims it and oracle_selftest pins the shape, because a helper the two
OSes answer differently is one that makes every pin reading it agree with
anything.

The hosts-wall leg asserted a socket path inside a label bar on an
80-column terminal. macOS puts its temporary files under a directory whose
name alone is 45 characters, so the bar elided exactly the path being
grepped for. The width now comes from the socket and the height follows it,
and a box with a short $TMPDIR still gets the 80x44 this has always used.

The pager leg anchored the view with a `+178g` flag, and attaching makes
less repaint: less 704 held the top at 178 where less 668 moved it to 179.
The 178 is typed into the pager after the attach instead, so the repaint
has nothing left to differ about.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

test/e2e_03_side.sh
Old New
@@ -221,9 +221,9 @@ PACOMM=$(pid_comm "$PAPID")
221 # rule the fork obeys — the child is the foreground daemon, and a `-d` 221 # rule the fork obeys — the child is the foreground daemon, and a `-d`
222 # still in its line would fork again. 222 # still in its line would fork again.
223 PACMD=$(pid_args "$PAPID") 223 PACMD=$(pid_args "$PAPID")
224 [ "$PACMD" = "mux d start --sock $SOCK15 " ] || { 224 [ "$PACMD" = "mux d start --sock $SOCK15" ] || {
225 echo "e2e FAIL: the auto-started daemon's argv is [$PACMD]," 225 echo "e2e FAIL: the auto-started daemon's argv is [$PACMD],"
226 echo " want [mux d start --sock $SOCK15 ]" 226 echo " want [mux d start --sock $SOCK15]"
227 exit 1; } 227 exit 1; }
228 assert_converged_pty "$OUT.pa" "$SOCK15" "local mux auto-start" 80 24 228 assert_converged_pty "$OUT.pa" "$SOCK15" "local mux auto-start" 80 24
229 # Same teardown, same reasons — and the stderr is captured rather than 229 # Same teardown, same reasons — and the stderr is captured rather than
test/e2e_08_mouse.sh
Old New
@@ -205,13 +205,15 @@ ok "an application that asked for the mouse gets the wheel, and the client does
205 # screenful, so the top moves to 154 and 178 leaves the screen entirely — 205 # screenful, so the top moves to 154 and 178 leaves the screen entirely —
206 # a needle and its negative. 206 # a needle and its negative.
207 # 207 #
208 # `+G` is what this leg used to spell, and it is the one thing here that 208 # The `178g` is typed INTO the pager after the attach, and it is what makes
209 # cannot be written down: it says "put the END of the file on screen", and 209 # 178 a fact rather than a hope. Attaching makes less repaint, and the
210 # where the top of that view lands depends on how many of the 24 rows the 210 # repaint does not always land where the flag put it: measured on this
211 # build spends on its own status line. less 704 (Debian) left the top at 211 # scenario, less 704 (Debian) held the top at 178 while less 668 (macOS 26)
212 # 178 and less 668 (macOS 26) at 179, so a leg that spelled 178 and 154 212 # moved it to 179, so a leg that read the top before the attach, or wrote
213 # failed on the Mac by exactly one row while the wheel worked perfectly. 213 # it down, was off by one row on the Mac while the wheel worked perfectly.
214 # `g` names the top row instead of deriving it, and both builds obey it. 214 # Re-anchoring after the repaint leaves nothing for a build to differ
215 # about. The `+178g` flag is still there so the wait loop below has a page
216 # to look for.
215 # 217 #
216 # LESS is cleared in the script rather than trusted: an operator with 218 # LESS is cleared in the script rather than trusted: an operator with
217 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse, 219 # `LESS=--mouse` exported would have a pager that DOES ask for the mouse,
@@ -245,6 +247,9 @@ hostroom pager
245 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \ 247 XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pgr" --err "$OUT.pgr.err" \
246 -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF' 248 -- "$MUX" --sock "$SOCK35" > "$OUT.pgr.log" 2>&1 <<'EOF'
247 expect 199 15000 249 expect 199 15000
250 settle 400 15000
251 send 178g
252 settle 600 15000
248 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M 253 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M
249 settle 800 15000 254 settle 800 15000
250 send \x1cd 255 send \x1cd
test/e2e_09_hosts.sh
Old New
@@ -268,16 +268,28 @@ wait_sessions "$SOCKH2" 2 "hosts: daemon 2 should hold 0 and c"
268 # every later assertion about it is about a session the wall could reach 268 # every later assertion about it is about a session the wall could reach
269 # and does not. 269 # and does not.
270 # 270 #
271 # Tall, and NOT wide: 80 columns is under the aspect rule's 271 # Tall, and NOT wide: the terminal stays under the aspect rule's
272 # `cols >= 2 * rows`, so a bar gets the full width. Beside, three stripes 272 # `cols >= 2 * rows`, so a bar gets the full width. Beside, three stripes
273 # would each be twenty-odd columns and `labelText` would spend them on the 273 # would each be twenty-odd columns and `labelText` would spend them on the
274 # state word — the socket path these assertions grep for is the first thing 274 # state word — the socket path these assertions grep for is the first thing
275 # a narrow bar drops. 275 # a narrow bar drops.
276 #
277 # Wide ENOUGH is the other half, and it cannot be a constant: the label is
278 # `--sock <socket> #<session> [up]` and $TMPDIR decides how long the socket
279 # is. It is under /tmp on Linux and 80 columns has always been plenty;
280 # macOS puts it in a per-user directory whose name alone is 45 characters,
281 # and the bar elided exactly the path being grepped for. So the width comes
282 # from the socket and the height follows it, and a box with a short $TMPDIR
283 # gets the 80x44 this has always used.
284 HWCOLS=$(( ${#SOCKH1} + 20 ))
285 [ "$HWCOLS" -ge 80 ] || HWCOLS=80
286 HWROWS=$(( HWCOLS / 2 + 4 ))
287 [ "$HWROWS" -ge 44 ] || HWROWS=44
276 seed_layout "$HSTATE" stacked \ 288 seed_layout "$HSTATE" stacked \
277 "--sock $SOCKH1#0" "--sock $SOCKH1#b" "--sock $SOCKH2#0" 289 "--sock $SOCKH1#0" "--sock $SOCKH1#b" "--sock $SOCKH2#0"
278 HFHASH=$(sha256_of "$HSTATE/mux/hosts") 290 HFHASH=$(sha256_of "$HSTATE/mux/hosts")
279 set +e 291 set +e
280 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 44 \ 292 XDG_STATE_HOME="$HSTATE" timeout 90 "$PTYCLIENT" --cols "$HWCOLS" --rows "$HWROWS" \
281 --out "$OUT.hwcap" --err "$OUT.hwcap.err" -- "$MUX" > "$OUT.hwpc" 2>&1 <<'EOF' 293 --out "$OUT.hwcap" --err "$OUT.hwcap.err" -- "$MUX" > "$OUT.hwpc" 2>&1 <<'EOF'
282 expect hw1-pin 25000 294 expect hw1-pin 25000
283 settle 1500 25000 295 settle 1500 25000
test/e2e_10_agent.sh
Old New
@@ -87,6 +87,15 @@ esac
87 # Unquoted heredoc, like the OSC 52 leg's and for the same reason: $FP48 has 87 # Unquoted heredoc, like the OSC 52 leg's and for the same reason: $FP48 has
88 # to interpolate. Nothing else in the script needs expanding — `\n` and 88 # to interpolate. Nothing else in the script needs expanding — `\n` and
89 # `\x1b` are not escapes the shell touches. 89 # `\x1b` are not escapes the shell touches.
90 #
91 # No BACKTICKS below this line, comments included, and that is a rule and
92 # not a preference: an unquoted heredoc runs `word` as a command
93 # substitution wherever it appears, and the comments here used to quote
94 # `x`, `d`, `expect` and `settle` the way the rest of this repo quotes a
95 # key or a verb. On Linux all four are "command not found" and the run
96 # carries on none the wiser. On macOS /usr/bin/expect exists, so the shell
97 # started it, it read the rest of the heredoc as its own script, and the
98 # group hung until its budget ran out (2026-09-04).
90 set +e 99 set +e
91 hostroom agt48 100 hostroom agt48
92 XDG_STATE_HOME="$HOSTROOM" SSH_AUTH_SOCK="$AGENT48" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 \ 101 XDG_STATE_HOME="$HOSTROOM" SSH_AUTH_SOCK="$AGENT48" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 \
@@ -98,14 +107,15 @@ send ssh-add -l\n
98 expect $FP48 15000 107 expect $FP48 15000
99 settle 400 15000 108 settle 400 15000
100 send exit\n 109 send exit\n
101 # On a TERMINAL the last tile's exit leaves an EMPTY WALL — `x` ends a 110 # On a TERMINAL the last tile's exit leaves an EMPTY WALL — the x key ends
102 # session, never mux — and the picker opens over it. Esc closes the popup 111 # a session, never mux — and the picker opens over it. Esc closes the popup
103 # and `d` is what leaves. A piped mux still exits with the shell's code. 112 # and the d key is what leaves. A piped mux still exits with the shell's
113 # code.
104 # 114 #
105 # `expect`, not `settle`: the popup is the CLAIM, so waiting for its legend 115 # An expect verb here rather than a settle: the popup is the CLAIM, so
106 # is what makes this leg fail if the exit ever ends mux again instead. The 116 # waiting for its legend is what makes this leg fail if the exit ever ends
107 # empty-wall scenario asserts a picker on a wall that never had a tile; 117 # mux again instead. The empty-wall scenario asserts a picker on a wall
108 # only this one sees the TRANSITION. 118 # that never had a tile; only this one sees the TRANSITION.
109 expect c new session 15000 119 expect c new session 15000
110 send \x1b 120 send \x1b
111 settle 300 15000 121 settle 300 15000
@@ -467,10 +477,10 @@ send echo b-ready\n
467 expect flip-now 30000 477 expect flip-now 30000
468 send ssh-add -l\n 478 send ssh-add -l\n
469 expect $FPB 20000 479 expect $FPB 20000
470 # A's `exit` ends the session both clients hold, so B's tile goes too and 480 # A's exit command ends the session both clients hold, so B's tile goes too and
471 # B is left on the same empty wall with the same picker over it. The 481 # B is left on the same empty wall with the same picker over it. The
472 # legend and not a settle, doubly so here: B is a passive observer, so its 482 # legend and not a settle, doubly so here: B is a passive observer, so its
473 # screen is already quiet when A types `exit` and a duration could expire 483 # screen is already quiet when A types exit and a duration could expire
474 # before the transition this waits for has happened at all. 484 # before the transition this waits for has happened at all.
475 expect c new session 20000 485 expect c new session 20000
476 send \x1b 486 send \x1b
test/oracle_selftest.sh
Old New
@@ -118,6 +118,14 @@ time.sleep(300)' \
118 echo "e2e FAIL: oracle: pid_comm of an sh is '$(pid_comm "$_opid")'"; exit 1; } 118 echo "e2e FAIL: oracle: pid_comm of an sh is '$(pid_comm "$_opid")'"; exit 1; }
119 pid_args "$_opid" | grep -q 'sleep 300' || { 119 pid_args "$_opid" | grep -q 'sleep 300' || {
120 echo "e2e FAIL: oracle: pid_args lost the argv: [$(pid_args "$_opid")]"; exit 1; } 120 echo "e2e FAIL: oracle: pid_args lost the argv: [$(pid_args "$_opid")]"; exit 1; }
121 # The SHAPE too, because the one caller that compares a whole argv does
122 # it with `=`: one space between words and none at the end. Linux reads
123 # a NUL-terminated /proc entry and Darwin reads ps, and the two agreed
124 # on every word and disagreed on the last byte until this was pinned.
125 case "$(pid_args "$_opid")" in
126 *' ') echo "e2e FAIL: oracle: pid_args ends in a space: [$(pid_args "$_opid")]"; exit 1 ;;
127 *' '*) echo "e2e FAIL: oracle: pid_args doubles a space: [$(pid_args "$_opid")]"; exit 1 ;;
128 esac
121 [ "$(pid_fd_count "$_opid")" -ge 5 ] || { 129 [ "$(pid_fd_count "$_opid")" -ge 5 ] || {
122 echo "e2e FAIL: oracle: pid_fd_count is $(pid_fd_count "$_opid") for a shell" 130 echo "e2e FAIL: oracle: pid_fd_count is $(pid_fd_count "$_opid") for a shell"
123 echo " holding fd 5 and fd 6 on top of the usual three"; exit 1; } 131 echo " holding fd 5 and fd 6 on top of the usual three"; exit 1; }
test/os_oracle.sh
Old New
@@ -228,7 +228,13 @@ print "$r\n";' "$1"
228 pid_alive() { kill -0 "$1" 2>/dev/null || [ -d "/proc/$1" ]; } 228 pid_alive() { kill -0 "$1" 2>/dev/null || [ -d "/proc/$1" ]; }
229 pid_exe() { readlink -f "/proc/$1/exe" 2>/dev/null; } 229 pid_exe() { readlink -f "/proc/$1/exe" 2>/dev/null; }
230 pid_comm() { cat "/proc/$1/comm" 2>/dev/null; } 230 pid_comm() { cat "/proc/$1/comm" 2>/dev/null; }
231 pid_args() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null; } 231 # The words with ONE space between them and none at the end. Every
232 # entry in /proc/PID/cmdline is NUL-TERMINATED, so a straight
233 # translation leaves a trailing separator that the Darwin arm's
234 # `ps -o args=` does not, and the one caller that compares the whole
235 # argv as a string then has to spell a space it can only have learned
236 # from Linux. Trimmed here so both arms answer the same shape.
237 pid_args() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null | sed 's/ *$//'; }
232 pid_children() { ps -o pid= --ppid "$1" 2>/dev/null | tr -d ' '; } 238 pid_children() { ps -o pid= --ppid "$1" 2>/dev/null | tr -d ' '; }
233 pid_fd_count() { find "/proc/$1/fd" -mindepth 1 2>/dev/null | wc -l | tr -d ' '; } 239 pid_fd_count() { find "/proc/$1/fd" -mindepth 1 2>/dev/null | wc -l | tr -d ' '; }
234 pid_fd_targets() { readlink "/proc/$1"/fd/* 2>/dev/null; } 240 pid_fd_targets() { readlink "/proc/$1"/fd/* 2>/dev/null; }
@@ -304,9 +310,16 @@ eval "$(now_ms_snippet)"
304 # answers 127 where GNU answers 126; a signal sent to the WRAPPER is not 310 # answers 127 where GNU answers 126; a signal sent to the WRAPPER is not
305 # relayed to the child, where GNU forwards TERM, INT and HUP; and the child 311 # relayed to the child, where GNU forwards TERM, INT and HUP; and the child
306 # is not put in a process group, so GNU's kill-the-group on expiry becomes 312 # is not put in a process group, so GNU's kill-the-group on expiry becomes
307 # kill-the-child and a grandchild outlives the budget. Every caller in this 313 # kill-the-child and a grandchild outlives the budget.
308 # tree runs `timeout <seconds> CMD` in the foreground over a single mux or 314 #
309 # ptyclient child, with no flags. 315 # Almost every caller in this tree runs `timeout <seconds> CMD` in the
316 # foreground over a single mux or ptyclient child, with no flags, and none
317 # of the three differences can reach one. The exceptions, and why they are
318 # still safe: e2e_01_boot.sh backgrounds one client and reaps it by pid
319 # rather than by signalling the wrapper; e2e_05_session.sh wraps a wsclient,
320 # which is one child like the others; and test/vm.sh, test/mac.sh and
321 # test/xos.sh wrap ssh, but those three drivers run on the Linux side of a
322 # gate, where `timeout` is the GNU binary and this arm is never defined.
310 if command -v timeout >/dev/null 2>&1; then 323 if command -v timeout >/dev/null 2>&1; then
311 : 324 :
312 elif command -v gtimeout >/dev/null 2>&1; then 325 elif command -v gtimeout >/dev/null 2>&1; then