8a3c3ba9
test: 11_select moves to a file of its own
a73x 2026-08-26 19:12
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -102,13 +102,13 @@ done | |||
| 102 | E2E_DIR=$(dirname "$0") | 102 | E2E_DIR=$(dirname "$0") |
| 103 | . "$E2E_DIR/e2e_lib.sh" | 103 | . "$E2E_DIR/e2e_lib.sh" |
| 104 | 104 | ||
| 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_wallhist 10_agent' | 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session 06_web 07_wallcli 08_mouse 09_wallhist 10_agent 11_select' |
| 106 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses | 106 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses |
| 107 | # these by name rather than running them: a group whose fixtures are | 107 | # these by name rather than running them: a group whose fixtures are |
| 108 | # missing does not fail, it passes having tested something else — and a | 108 | # missing does not fail, it passes having tested something else — and a |
| 109 | # check that passes without running is the one failure a green tree cannot | 109 | # check that passes without running is the one failure a green tree cannot |
| 110 | # show. | 110 | # show. |
| 111 | E2E_NEEDS='03_side:01_boot' | 111 | E2E_NEEDS='03_side:01_boot 11_select:10_agent' |
| 112 | 112 | ||
| 113 | # E2E_ONLY=<group> — run one group file and stop. For the loop a leg is in | 113 | # E2E_ONLY=<group> — run one group file and stop. For the loop a leg is in |
| 114 | # while it is being written; the gate is still the whole suite, which is | 114 | # while it is being written; the gate is still the whole suite, which is |
| @@ -145,391 +145,6 @@ for _g in $E2E_GROUPS; do | |||
| 145 | . "$E2E_DIR/e2e_$_g.sh" | 145 | . "$E2E_DIR/e2e_$_g.sh" |
| 146 | done | 146 | done |
| 147 | 147 | ||
| 148 | # The wall's own mouse claim needs a daemon of its own for the same reason | ||
| 149 | # every other leg does: the wall scenario above stops its daemon on the way | ||
| 150 | # out, so there is nothing left to reuse. | ||
| 151 | SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock" | ||
| 152 | defer_sock "$SOCK50" | ||
| 153 | |||
| 154 | # ---- the SOCK50 cluster: a daemon, two named sessions ------------------ | ||
| 155 | # | ||
| 156 | # A daemon and two named sessions (a, b) shared by the click leg below and | ||
| 157 | # the drag legs after it. The wall-mouse filter this cluster used to open | ||
| 158 | # with is gone — every click and drag routes to the focused tile's own | ||
| 159 | # Core, hit-tested by rect — so what remains here is the setup: a daemon, | ||
| 160 | # a marker per session, and the tiles a `mux wall` over them paints. | ||
| 161 | start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh | ||
| 162 | D48PID=$DPID | ||
| 163 | { printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ | ||
| 164 | timeout 40 "$MUX" --sock "$SOCK50" --session a > "$OUT.wmsa" 2> "$OUT.wmsa.err" | ||
| 165 | wait_grid "$SOCK50" "wma-pin" "wall cluster: session a's marker" a | ||
| 166 | { printf 'printf "wmb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ | ||
| 167 | timeout 40 "$MUX" --sock "$SOCK50" --session b > "$OUT.wmsb" 2> "$OUT.wmsb.err" | ||
| 168 | wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b | ||
| 169 | |||
| 170 | # ...and a click in a tile's rect FOCUSES it. Two tiles over 30 rows is two | ||
| 171 | # fifteen-row stripes, each opening with a label bar, so terminal row 19 is | ||
| 172 | # inside session b's content and terminal row 2 is inside session a's. | ||
| 173 | # | ||
| 174 | # Which tile the focus is ON is read off the DAEMON, never off the paint: a | ||
| 175 | # marker typed after the click lands in the focused session and `muxa | ||
| 176 | # capture` says which one that was. The report bytes themselves go through | ||
| 177 | # to the focused tile's Core unchanged, so the Core that owns the drag sees | ||
| 178 | # them. | ||
| 179 | # | ||
| 180 | # Two runs. The first clicks b's stripe from a fresh wall (focus starts on | ||
| 181 | # a) and proves the click moved the focus to b; the second focuses b with | ||
| 182 | # `Ctrl-\ 2` and clicks a's stripe, proving a click moves the focus the | ||
| 183 | # other way. A negative that cannot fail is not a test, so each carries the | ||
| 184 | # marker that MUST land in the focused session. | ||
| 185 | set +e | ||
| 186 | timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \ | ||
| 187 | "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc3" 2>&1 <<'EOF' | ||
| 188 | expect wmb-pin 20000 | ||
| 189 | settle 700 20000 | ||
| 190 | send \x1b[<0;3;19M\x1b[<0;3;19m | ||
| 191 | settle 700 20000 | ||
| 192 | send printf 'wm-%s\\n' three\n | ||
| 193 | expect wm-three 15000 | ||
| 194 | settle 400 15000 | ||
| 195 | send \x1cd | ||
| 196 | waitexit 10000 | ||
| 197 | EOF | ||
| 198 | RC=$? | ||
| 199 | set -e | ||
| 200 | [ "$RC" -eq 0 ] || { | ||
| 201 | echo "e2e FAIL: wall click: ptyclient leg exited $RC (did the click focus b?):" | ||
| 202 | cat "$OUT.wmpc3"; exit 1; } | ||
| 203 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 | ||
| 204 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 | ||
| 205 | grep -q "wm-three" "$OUT.wmfb" || { | ||
| 206 | echo "e2e FAIL: wall click: a click in session b's stripe did not focus it:" | ||
| 207 | cat "$OUT.wmfb"; exit 1; } | ||
| 208 | grep -q "wm-three" "$OUT.wmfa" && { | ||
| 209 | echo "e2e FAIL: wall click: the focus stayed on session a:" | ||
| 210 | cat "$OUT.wmfa"; exit 1; } | ||
| 211 | |||
| 212 | # The other direction: focus b explicitly, then click a's content and watch | ||
| 213 | # the focus come back to a. | ||
| 214 | set +e | ||
| 215 | timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \ | ||
| 216 | "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc4" 2>&1 <<'EOF' | ||
| 217 | expect wmb-pin 20000 | ||
| 218 | settle 700 20000 | ||
| 219 | send \x1c2 | ||
| 220 | settle 700 20000 | ||
| 221 | send \x1b[<0;3;2M\x1b[<0;3;2m | ||
| 222 | settle 700 20000 | ||
| 223 | send printf 'wm-%s\\n' four\n | ||
| 224 | expect wm-four 15000 | ||
| 225 | settle 400 15000 | ||
| 226 | send \x1cd | ||
| 227 | waitexit 10000 | ||
| 228 | EOF | ||
| 229 | RC=$? | ||
| 230 | set -e | ||
| 231 | [ "$RC" -eq 0 ] || { | ||
| 232 | echo "e2e FAIL: wall click: label-bar leg exited $RC:" | ||
| 233 | cat "$OUT.wmpc4"; exit 1; } | ||
| 234 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 | ||
| 235 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 | ||
| 236 | grep -q "wm-four" "$OUT.wmfa" || { | ||
| 237 | echo "e2e FAIL: wall click: a click in session a's stripe did not focus it:" | ||
| 238 | cat "$OUT.wmfa"; exit 1; } | ||
| 239 | grep -q "wm-four" "$OUT.wmfb" && { | ||
| 240 | echo "e2e FAIL: wall click: the click never moved the focus off session b:" | ||
| 241 | cat "$OUT.wmfb"; exit 1; } | ||
| 242 | ok "a click in a tile's rect focuses it, from wherever the focus was" | ||
| 243 | |||
| 244 | # A beside layout gives every tile the same rows, so a click that tests | ||
| 245 | # only the row hits the first present tile every time. A click in the | ||
| 246 | # RIGHT pane's content — column, not row — is what proves the column | ||
| 247 | # test in rectHit earns its keep: focus starts on a (the left pane) and | ||
| 248 | # the click must move it to b (the right pane). | ||
| 249 | set +e | ||
| 250 | timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \ | ||
| 251 | "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc5" 2>&1 <<'EOF' | ||
| 252 | expect wmb-pin 20000 | ||
| 253 | settle 700 20000 | ||
| 254 | send \x1b[<0;70;9M\x1b[<0;70;9m | ||
| 255 | settle 700 20000 | ||
| 256 | send printf 'wm-%s\\n' five\n | ||
| 257 | expect wm-five 15000 | ||
| 258 | settle 400 15000 | ||
| 259 | send \x1cd | ||
| 260 | waitexit 10000 | ||
| 261 | EOF | ||
| 262 | RC=$? | ||
| 263 | set -e | ||
| 264 | [ "$RC" -eq 0 ] || { | ||
| 265 | echo "e2e FAIL: wall click: beside leg exited $RC (did the column click focus b?):" | ||
| 266 | cat "$OUT.wmpc5"; exit 1; } | ||
| 267 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 | ||
| 268 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 | ||
| 269 | grep -q "wm-five" "$OUT.wmfb" || { | ||
| 270 | echo "e2e FAIL: wall click: a click in the right pane did not focus it:" | ||
| 271 | cat "$OUT.wmfb"; exit 1; } | ||
| 272 | grep -q "wm-five" "$OUT.wmfa" && { | ||
| 273 | echo "e2e FAIL: wall click: the focus stayed on the left pane:" | ||
| 274 | cat "$OUT.wmfa"; exit 1; } | ||
| 275 | ok "a click in a beside pane finds it by column, not just row" | ||
| 276 | |||
| 277 | # ---- a drag copies, and the copy leaves as OSC 52 ---------------------- | ||
| 278 | # | ||
| 279 | # The first end-to-end proof that this feature does what it was asked for. | ||
| 280 | # The highlight legs above could not have one: a highlight is a PAINT, and | ||
| 281 | # these legs read the daemon rather than the screen. A copy is different — | ||
| 282 | # it lands on the CLIENT's tty as OSC 52, which the fixture holding that | ||
| 283 | # tty can read, so the assertion is the copied text itself. | ||
| 284 | # | ||
| 285 | # A one-tile wall, because that is where the geometry is knowable: `mux | ||
| 286 | # TARGET` is a wall of one tile whose rect is the whole terminal (no label | ||
| 287 | # bar), so a terminal row IS a grid row and the text can be put on one by | ||
| 288 | # name. The session is given its content with an explicit cursor address | ||
| 289 | # rather than by printing lines, so the row this drag crosses does not | ||
| 290 | # depend on where a prompt happened to leave the cursor. | ||
| 291 | # | ||
| 292 | # The typed line is `ZZ-%s` and the printed line is `ZZ-COPYME`, so the | ||
| 293 | # expect below cannot be satisfied by the shell ECHOING the command back. | ||
| 294 | # | ||
| 295 | # Columns 1 to 9 and no further: the prompt lands at column 10 on the same | ||
| 296 | # row, and a selection that reached it would copy the prompt too. | ||
| 297 | set +e | ||
| 298 | timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.selcap" --err "$OUT.selcap.err" -- \ | ||
| 299 | "$MUX" --sock "$SOCK50" --session s > "$OUT.selpc" 2>&1 <<'EOF' | ||
| 300 | settle 900 20000 | ||
| 301 | send printf '\\033[2J\\033[9;1HZZ-%s' COPYME\n | ||
| 302 | expect ZZ-COPYME 15000 | ||
| 303 | settle 700 20000 | ||
| 304 | send \x1b[<0;3;9M\x1b[<0;3;9m | ||
| 305 | settle 500 15000 | ||
| 306 | send \x1b[<0;1;9M\x1b[<32;9;9M\x1b[<0;9;9m | ||
| 307 | expect \x1b]52;c;WlotQ09QWU1F 15000 | ||
| 308 | send \x1cd | ||
| 309 | waitexit 10000 | ||
| 310 | EOF | ||
| 311 | RC=$? | ||
| 312 | set -e | ||
| 313 | [ "$RC" -eq 0 ] || { | ||
| 314 | echo "e2e FAIL: drag copy: ptyclient leg exited $RC (no OSC 52 with the dragged text?):" | ||
| 315 | cat "$OUT.selpc"; exit 1; } | ||
| 316 | # Exactly one copy on that tty, which is what makes the CLICK above a | ||
| 317 | # negative rather than decoration: a click highlights nothing, so it has | ||
| 318 | # nothing to copy, and a client that copied on every button release would | ||
| 319 | # have written two. | ||
| 320 | SELCOPIES=$(grep -ao "$(printf '\033]52;')" "$OUT.selcap" | wc -l) | ||
| 321 | [ "$SELCOPIES" = "1" ] || { | ||
| 322 | echo "e2e FAIL: drag copy: $SELCOPIES OSC 52 writes on the client's tty, expected 1"; exit 1; } | ||
| 323 | # ---- a drag over WIDE cells leaves the client's screen converged ------- | ||
| 324 | # | ||
| 325 | # The gate the wide-cell shift walked straight through. Every other | ||
| 326 | # assertion this feature has is either on the daemon's state or on the | ||
| 327 | # ESCAPE BYTES the painter emitted — and a row that emits the same wide | ||
| 328 | # glyph TWICE still contains every substring the painter is supposed to | ||
| 329 | # emit, while landing a column wider than the grid. Only a check that | ||
| 330 | # judges the client's screen against the daemon's own, through a separate | ||
| 331 | # engine, can see that: which is assert_converged, and which no selection | ||
| 332 | # leg used until this one. | ||
| 333 | # | ||
| 334 | # Both edges of the drag land inside a wide cell on purpose. Row 3 is | ||
| 335 | # `w漢字x-WIDEMARK`, so column 3 is the spacer tail of 漢 and column 4 the | ||
| 336 | # first half of 字 — the two ways a span can cut a wide cell in half. | ||
| 337 | # | ||
| 338 | # The DEFAULT session, unlike the copy leg above: `converged_quiet` dumps | ||
| 339 | # the daemon's default grid, so a leg that named a session would diff this | ||
| 340 | # client's screen against a grid it never attached to and fail for a reason | ||
| 341 | # that has nothing to do with wide cells. | ||
| 342 | # | ||
| 343 | # Asserted to fail, per the wan.sh rule: with `snapWide` stubbed out to | ||
| 344 | # return its arguments, this leg renders `w 漢字x-WIDEMARK` against the | ||
| 345 | # daemon's `w漢字x-WIDEMARK` — the extra column, which is the bug. | ||
| 346 | # | ||
| 347 | # The click after the drag is what makes the STYLED half of this check | ||
| 348 | # mean something. A live highlight is reverse video the daemon has no | ||
| 349 | # notion of, so a leg that detached still holding one would diverge on | ||
| 350 | # style every time and could only ever assert the plain grid. Clearing it | ||
| 351 | # first buys the byte-exact comparison, and with it the assertion that a | ||
| 352 | # highlight leaves nothing behind when it goes — the overlay-never-becomes- | ||
| 353 | # state rule, on the only overlay that paints through the replica. | ||
| 354 | set +e | ||
| 355 | timeout 60 "$PTYCLIENT" --cols 40 --rows 12 --out "$OUT.swcap" --err "$OUT.swcap.err" -- \ | ||
| 356 | "$MUX" --sock "$SOCK50" > "$OUT.swpc" 2>&1 <<'EOF' | ||
| 357 | settle 900 20000 | ||
| 358 | send printf '\\033[2J\\033[3;1Hw\\346\\274\\242\\345\\255\\227x-WIDEMARK'\n | ||
| 359 | expect WIDEMARK 15000 | ||
| 360 | settle 700 20000 | ||
| 361 | send \x1b[<0;3;3M\x1b[<32;4;3M\x1b[<0;4;3m | ||
| 362 | settle 700 20000 | ||
| 363 | send \x1b[<0;1;3M\x1b[<0;1;3m | ||
| 364 | settle 700 20000 | ||
| 365 | send \x1cd | ||
| 366 | waitexit 10000 | ||
| 367 | EOF | ||
| 368 | RC=$? | ||
| 369 | set -e | ||
| 370 | [ "$RC" -eq 0 ] || { | ||
| 371 | echo "e2e FAIL: wide drag: ptyclient leg exited $RC:" | ||
| 372 | # The client's own stderr as well as the fixture's: the fixture can only | ||
| 373 | # report THAT the client closed the pty, and the reason is over here. | ||
| 374 | cat "$OUT.swpc" "$OUT.swcap.err"; exit 1; } | ||
| 375 | assert_converged "$OUT.swcap" "$SOCK50" "wide drag" 40 12 | ||
| 376 | ok "a drag across wide cells leaves the client's screen converged" | ||
| 377 | |||
| 378 | assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop" | ||
| 379 | D48PID="" | ||
| 380 | ok "a drag copies on release, and a click copies nothing" | ||
| 381 | |||
| 382 | # The in-band resize leg: a daemon of its own, so the grid it reports on is | ||
| 383 | # the one this leg sized. | ||
| 384 | SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock" | ||
| 385 | defer_sock "$SOCK51" | ||
| 386 | |||
| 387 | # --- a resize is told in-band to an app that asked for it (mode 2048) ------ | ||
| 388 | # | ||
| 389 | # nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and | ||
| 390 | # the engine says "recognised" — so nvim turns the mode on and from then on | ||
| 391 | # IGNORES SIGWINCH, waiting for `CSI 48;rows;cols;h;w t` on its input. The | ||
| 392 | # daemon never sent one, and a resized session stayed painted at its old | ||
| 393 | # size until nvim was restarted (found by hand, 2026-08-22, a drag-resized | ||
| 394 | # window full of two overlaid screens). | ||
| 395 | # | ||
| 396 | # The witness needs no nvim: `cat -v` prints whatever arrives on its stdin, | ||
| 397 | # so the report — INPUT to the app — lands on the grid as text. Two reports | ||
| 398 | # are asserted, because they are two code paths: the one the mode-set | ||
| 399 | # itself owes (the app sizes itself from it), and the one each resize owes. | ||
| 400 | start_daemon "$SOCK51" "$OUT.inband.d" "in-band daemon never bound" --shell /bin/sh | ||
| 401 | D51PID=$DPID | ||
| 402 | set +e | ||
| 403 | timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.inband" --err "$OUT.inband.err" -- \ | ||
| 404 | "$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF' | ||
| 405 | expect \x1b[?1049h 15000 | ||
| 406 | settle 400 15000 | ||
| 407 | send printf '\\033[?2048h'; cat -v\n | ||
| 408 | expect [48;30;100;0;0t 10000 | ||
| 409 | resize 120 40 | ||
| 410 | expect [48;40;120;0;0t 10000 | ||
| 411 | settle 500 15000 | ||
| 412 | send \x1c\x1c | ||
| 413 | waitexit 10000 | ||
| 414 | EOF | ||
| 415 | RC=$? | ||
| 416 | set -e | ||
| 417 | [ "$RC" -eq 0 ] || { | ||
| 418 | echo "e2e FAIL: in-band size: the leg exited $RC:"; cat "$OUT.inband.log"; exit 1; } | ||
| 419 | assert_stopped "$SOCK51" "$D51PID" "in-band size" "$OUT.inbstop" | ||
| 420 | D51PID="" | ||
| 421 | ok "a resize is reported in-band to an app that turned on mode 2048" | ||
| 422 | |||
| 423 | # --- The default socket path refuses to be guessed. Field incident: a tmux | ||
| 424 | # server started before logind exported XDG_RUNTIME_DIR handed every pane an | ||
| 425 | # environment without it, so a pane's `muxd stop` looked at the /tmp guess | ||
| 426 | # while the daemon a pane had started owned $XDG_RUNTIME_DIR/muxd.sock — one | ||
| 427 | # uid, one box, two daemons. `env -u` rather than the harness environment: | ||
| 428 | # on a box that HAS the variable the unset case is otherwise never taken. | ||
| 429 | # No convergence: nothing gets far enough to attach. | ||
| 430 | for BIN in "$MUXD stats" "$MUX"; do | ||
| 431 | # shellcheck disable=SC2086 # the two-word muxd form is deliberate | ||
| 432 | if env -u XDG_RUNTIME_DIR $BIN > "$OUT.nort" 2>&1; then | ||
| 433 | echo "e2e FAIL: '$BIN' with no XDG_RUNTIME_DIR did not refuse"; exit 1 | ||
| 434 | fi | ||
| 435 | grep -q 'XDG_RUNTIME_DIR is unset' "$OUT.nort" || { | ||
| 436 | echo "e2e FAIL: '$BIN' refusal does not name the variable:"; cat "$OUT.nort"; exit 1; } | ||
| 437 | grep -q -- '--sock' "$OUT.nort" || { | ||
| 438 | echo "e2e FAIL: '$BIN' refusal does not say how to fix it:"; cat "$OUT.nort"; exit 1; } | ||
| 439 | done | ||
| 440 | # muxa answers agents in JSON, so its refusal has to arrive as a reply. | ||
| 441 | env -u XDG_RUNTIME_DIR "$MUXA" status > "$OUT.nort.a" 2>&1 && { | ||
| 442 | echo "e2e FAIL: muxa with no XDG_RUNTIME_DIR did not refuse"; exit 1; } | ||
| 443 | grep -q '"error":"no default socket path"' "$OUT.nort.a" || { | ||
| 444 | echo "e2e FAIL: muxa refusal is not a JSON error:"; cat "$OUT.nort.a"; exit 1; } | ||
| 445 | # The socketless verbs must survive the same environment: --version is how an | ||
| 446 | # operator checks whether the scp landed, and it owes nothing to a socket. | ||
| 447 | env -u XDG_RUNTIME_DIR "$MUXD" --version | grep -q '^muxd 0\.' || { | ||
| 448 | echo "e2e FAIL: muxd --version needs XDG_RUNTIME_DIR"; exit 1; } | ||
| 449 | env -u XDG_RUNTIME_DIR "$MUX" --version | grep -q '^mux 0\.' || { | ||
| 450 | echo "e2e FAIL: mux --version needs XDG_RUNTIME_DIR"; exit 1; } | ||
| 451 | rm -f "$OUT.nort" "$OUT.nort.a" | ||
| 452 | ok "no XDG_RUNTIME_DIR: the default path is refused by name, --version is not" | ||
| 453 | |||
| 454 | # --- an offerer that cannot answer is hung up on, and ssh falls through ---- | ||
| 455 | # | ||
| 456 | # An agent_offer is a declaration, not a capability (decisions.md | ||
| 457 | # 2026-08-22). The offerer here is a real `mux -A` whose agent is a real | ||
| 458 | # ssh-agent under SIGSTOP: the kernel still completes the connect from the | ||
| 459 | # listen backlog, so the -A preflight — which fails OPEN on silence, by | ||
| 460 | # design — lets the client through, and every request the session forwards | ||
| 461 | # to it then vanishes. That is the mute peer the daemon's answer clock | ||
| 462 | # exists for, produced with nothing but the binaries this suite already | ||
| 463 | # requires. | ||
| 464 | # | ||
| 465 | # Measured by hand for the issue: ssh-add against a socket that accepts and | ||
| 466 | # never replies blocks past 8s, against one that accepts and closes it | ||
| 467 | # fails in 2ms. So the two things asserted are the two halves of the fix: | ||
| 468 | # the FIRST ssh-add comes back inside `timeout 8` (the daemon closed the | ||
| 469 | # channel for the client that would not), and the SECOND is refused in | ||
| 470 | # milliseconds (the offer was taken away, so nobody routes to the mute | ||
| 471 | # client again and ssh pays nothing). | ||
| 472 | start_daemon "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound" --shell /bin/sh | ||
| 473 | D42PID=$DPID | ||
| 474 | ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1 | ||
| 475 | AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agtmute.env") | ||
| 476 | defer_kill "$AGENT48PID" | ||
| 477 | [ -n "$AGENT48PID" ] || { | ||
| 478 | echo "e2e FAIL: agent-mute: ssh-agent printed no pid to stop:" | ||
| 479 | cat "$OUT.agtmute.env"; exit 1; } | ||
| 480 | kill -STOP "$AGENT48PID" | ||
| 481 | set +e | ||
| 482 | SSH_AUTH_SOCK="$AGENT48" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 483 | --out "$OUT.agtmute" --err "$OUT.agtmute.err" \ | ||
| 484 | -- "$MUX" -A --sock "$SOCK48" > "$OUT.agtmute.log" 2>&1 <<'EOF' | ||
| 485 | expect \x1b[?1049h 15000 | ||
| 486 | settle 400 15000 | ||
| 487 | send timeout 8 ssh-add -l; echo mu""te1=$?\n | ||
| 488 | expect mute1= 20000 | ||
| 489 | send S=$(date +%s%N); ssh-add -l; R=$?; echo mu""te2=$R ms=$(( ($(date +%s%N)-S)/1000000 ))\n | ||
| 490 | expect mute2= 15000 | ||
| 491 | settle 400 15000 | ||
| 492 | send exit\n | ||
| 493 | waitexit 10000 | ||
| 494 | EOF | ||
| 495 | RC=$? | ||
| 496 | set -e | ||
| 497 | # The agent is resumed before it is killed: a stopped process ignores | ||
| 498 | # SIGTERM until it runs again, and the trap's plain kill would leave it. | ||
| 499 | kill -CONT "$AGENT48PID" 2>/dev/null || true | ||
| 500 | softkill "$AGENT48PID" || true | ||
| 501 | AGENT48PID="" | ||
| 502 | # The needles are split in the typed lines (`mu""te1=`) so an expect can | ||
| 503 | # only be satisfied by the command's OUTPUT, never by the pty's echo of the | ||
| 504 | # command — with an 8s wait in play, matching the echo runs the script | ||
| 505 | # ahead into a shell that is still busy. The wedge is checked before the | ||
| 506 | # exit code, because a wedged second ssh-add also swallows the `exit`. | ||
| 507 | grep -q "mute1=124" "$OUT.agtmute" && { | ||
| 508 | echo "e2e FAIL: agent-mute: the first ssh-add wedged for the whole 8s — the" | ||
| 509 | echo " daemon never hung up on the client that would not answer:" | ||
| 510 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 511 | [ "$RC" -eq 0 ] || { | ||
| 512 | echo "e2e FAIL: agent-mute: the leg exited $RC:" | ||
| 513 | cat "$OUT.agtmute.log"; exit 1; } | ||
| 514 | grep -qE "mute1=[1-9]" "$OUT.agtmute" || { | ||
| 515 | echo "e2e FAIL: agent-mute: the first ssh-add did not fail at all — something" | ||
| 516 | echo " answered for a stopped agent:" | ||
| 517 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 518 | MUTE2MS=$(sed -n 's/.*mute2=[0-9]* ms=\([0-9]*\).*/\1/p' "$OUT.agtmute" | head -1) | ||
| 519 | [ -n "$MUTE2MS" ] || { | ||
| 520 | echo "e2e FAIL: agent-mute: the second ssh-add reported no timing:" | ||
| 521 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 522 | # Well under the 5s bound, not merely under 8s: a refusal is the dial being | ||
| 523 | # closed at accept, and the only way to take 5s here is to be routed to the | ||
| 524 | # mute client again, which means the offer was not cleared. | ||
| 525 | [ "$MUTE2MS" -lt 2000 ] || { | ||
| 526 | echo "e2e FAIL: agent-mute: the second ssh-add took ${MUTE2MS}ms — the mute" | ||
| 527 | echo " client kept its offer and ssh paid the bound again" | ||
| 528 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 529 | wait_pid_gone "$D42PID" "agent-mute: the session ended and the daemon should follow" | ||
| 530 | D42PID="" | ||
| 531 | ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)" | ||
| 532 | |||
| 533 | # The scrollback-rect leg: a focused tile's history page must own only its | 148 | # The scrollback-rect leg: a focused tile's history page must own only its |
| 534 | # sub-rect, so its daemon gets a socket of its own for the grid-size | 149 | # sub-rect, so its daemon gets a socket of its own for the grid-size |
| 535 | # reason every wall leg has. | 150 | # reason every wall leg has. |
test/e2e_11_select.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,388 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_11_select.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in | ||
| 3 | # the order they stand in; see the lib's header for what this file may | ||
| 4 | # assume and what it must register. | ||
| 5 | # The wall's own mouse claim needs a daemon of its own for the same reason | ||
| 6 | # every other leg does: the wall scenario above stops its daemon on the way | ||
| 7 | # out, so there is nothing left to reuse. | ||
| 8 | SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock" | ||
| 9 | defer_sock "$SOCK50" | ||
| 10 | |||
| 11 | # ---- the SOCK50 cluster: a daemon, two named sessions ------------------ | ||
| 12 | # | ||
| 13 | # A daemon and two named sessions (a, b) shared by the click leg below and | ||
| 14 | # the drag legs after it. The wall-mouse filter this cluster used to open | ||
| 15 | # with is gone — every click and drag routes to the focused tile's own | ||
| 16 | # Core, hit-tested by rect — so what remains here is the setup: a daemon, | ||
| 17 | # a marker per session, and the tiles a `mux wall` over them paints. | ||
| 18 | start_daemon "$SOCK50" "$OUT.wmse.d" "wall-cluster daemon never bound" --shell /bin/sh | ||
| 19 | D48PID=$DPID | ||
| 20 | { printf 'printf "wma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ | ||
| 21 | timeout 40 "$MUX" --sock "$SOCK50" --session a > "$OUT.wmsa" 2> "$OUT.wmsa.err" | ||
| 22 | wait_grid "$SOCK50" "wma-pin" "wall cluster: session a's marker" a | ||
| 23 | { printf 'printf "wmb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ | ||
| 24 | timeout 40 "$MUX" --sock "$SOCK50" --session b > "$OUT.wmsb" 2> "$OUT.wmsb.err" | ||
| 25 | wait_grid "$SOCK50" "wmb-pin" "wall cluster: session b's marker" b | ||
| 26 | |||
| 27 | # ...and a click in a tile's rect FOCUSES it. Two tiles over 30 rows is two | ||
| 28 | # fifteen-row stripes, each opening with a label bar, so terminal row 19 is | ||
| 29 | # inside session b's content and terminal row 2 is inside session a's. | ||
| 30 | # | ||
| 31 | # Which tile the focus is ON is read off the DAEMON, never off the paint: a | ||
| 32 | # marker typed after the click lands in the focused session and `muxa | ||
| 33 | # capture` says which one that was. The report bytes themselves go through | ||
| 34 | # to the focused tile's Core unchanged, so the Core that owns the drag sees | ||
| 35 | # them. | ||
| 36 | # | ||
| 37 | # Two runs. The first clicks b's stripe from a fresh wall (focus starts on | ||
| 38 | # a) and proves the click moved the focus to b; the second focuses b with | ||
| 39 | # `Ctrl-\ 2` and clicks a's stripe, proving a click moves the focus the | ||
| 40 | # other way. A negative that cannot fail is not a test, so each carries the | ||
| 41 | # marker that MUST land in the focused session. | ||
| 42 | set +e | ||
| 43 | timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap3" --err "$OUT.wmcap3.err" -- \ | ||
| 44 | "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc3" 2>&1 <<'EOF' | ||
| 45 | expect wmb-pin 20000 | ||
| 46 | settle 700 20000 | ||
| 47 | send \x1b[<0;3;19M\x1b[<0;3;19m | ||
| 48 | settle 700 20000 | ||
| 49 | send printf 'wm-%s\\n' three\n | ||
| 50 | expect wm-three 15000 | ||
| 51 | settle 400 15000 | ||
| 52 | send \x1cd | ||
| 53 | waitexit 10000 | ||
| 54 | EOF | ||
| 55 | RC=$? | ||
| 56 | set -e | ||
| 57 | [ "$RC" -eq 0 ] || { | ||
| 58 | echo "e2e FAIL: wall click: ptyclient leg exited $RC (did the click focus b?):" | ||
| 59 | cat "$OUT.wmpc3"; exit 1; } | ||
| 60 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 | ||
| 61 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 | ||
| 62 | grep -q "wm-three" "$OUT.wmfb" || { | ||
| 63 | echo "e2e FAIL: wall click: a click in session b's stripe did not focus it:" | ||
| 64 | cat "$OUT.wmfb"; exit 1; } | ||
| 65 | grep -q "wm-three" "$OUT.wmfa" && { | ||
| 66 | echo "e2e FAIL: wall click: the focus stayed on session a:" | ||
| 67 | cat "$OUT.wmfa"; exit 1; } | ||
| 68 | |||
| 69 | # The other direction: focus b explicitly, then click a's content and watch | ||
| 70 | # the focus come back to a. | ||
| 71 | set +e | ||
| 72 | timeout 60 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.wmcap4" --err "$OUT.wmcap4.err" -- \ | ||
| 73 | "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc4" 2>&1 <<'EOF' | ||
| 74 | expect wmb-pin 20000 | ||
| 75 | settle 700 20000 | ||
| 76 | send \x1c2 | ||
| 77 | settle 700 20000 | ||
| 78 | send \x1b[<0;3;2M\x1b[<0;3;2m | ||
| 79 | settle 700 20000 | ||
| 80 | send printf 'wm-%s\\n' four\n | ||
| 81 | expect wm-four 15000 | ||
| 82 | settle 400 15000 | ||
| 83 | send \x1cd | ||
| 84 | waitexit 10000 | ||
| 85 | EOF | ||
| 86 | RC=$? | ||
| 87 | set -e | ||
| 88 | [ "$RC" -eq 0 ] || { | ||
| 89 | echo "e2e FAIL: wall click: label-bar leg exited $RC:" | ||
| 90 | cat "$OUT.wmpc4"; exit 1; } | ||
| 91 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 | ||
| 92 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 | ||
| 93 | grep -q "wm-four" "$OUT.wmfa" || { | ||
| 94 | echo "e2e FAIL: wall click: a click in session a's stripe did not focus it:" | ||
| 95 | cat "$OUT.wmfa"; exit 1; } | ||
| 96 | grep -q "wm-four" "$OUT.wmfb" && { | ||
| 97 | echo "e2e FAIL: wall click: the click never moved the focus off session b:" | ||
| 98 | cat "$OUT.wmfb"; exit 1; } | ||
| 99 | ok "a click in a tile's rect focuses it, from wherever the focus was" | ||
| 100 | |||
| 101 | # A beside layout gives every tile the same rows, so a click that tests | ||
| 102 | # only the row hits the first present tile every time. A click in the | ||
| 103 | # RIGHT pane's content — column, not row — is what proves the column | ||
| 104 | # test in rectHit earns its keep: focus starts on a (the left pane) and | ||
| 105 | # the click must move it to b (the right pane). | ||
| 106 | set +e | ||
| 107 | timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \ | ||
| 108 | "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc5" 2>&1 <<'EOF' | ||
| 109 | expect wmb-pin 20000 | ||
| 110 | settle 700 20000 | ||
| 111 | send \x1b[<0;70;9M\x1b[<0;70;9m | ||
| 112 | settle 700 20000 | ||
| 113 | send printf 'wm-%s\\n' five\n | ||
| 114 | expect wm-five 15000 | ||
| 115 | settle 400 15000 | ||
| 116 | send \x1cd | ||
| 117 | waitexit 10000 | ||
| 118 | EOF | ||
| 119 | RC=$? | ||
| 120 | set -e | ||
| 121 | [ "$RC" -eq 0 ] || { | ||
| 122 | echo "e2e FAIL: wall click: beside leg exited $RC (did the column click focus b?):" | ||
| 123 | cat "$OUT.wmpc5"; exit 1; } | ||
| 124 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1 | ||
| 125 | timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1 | ||
| 126 | grep -q "wm-five" "$OUT.wmfb" || { | ||
| 127 | echo "e2e FAIL: wall click: a click in the right pane did not focus it:" | ||
| 128 | cat "$OUT.wmfb"; exit 1; } | ||
| 129 | grep -q "wm-five" "$OUT.wmfa" && { | ||
| 130 | echo "e2e FAIL: wall click: the focus stayed on the left pane:" | ||
| 131 | cat "$OUT.wmfa"; exit 1; } | ||
| 132 | ok "a click in a beside pane finds it by column, not just row" | ||
| 133 | |||
| 134 | # ---- a drag copies, and the copy leaves as OSC 52 ---------------------- | ||
| 135 | # | ||
| 136 | # The first end-to-end proof that this feature does what it was asked for. | ||
| 137 | # The highlight legs above could not have one: a highlight is a PAINT, and | ||
| 138 | # these legs read the daemon rather than the screen. A copy is different — | ||
| 139 | # it lands on the CLIENT's tty as OSC 52, which the fixture holding that | ||
| 140 | # tty can read, so the assertion is the copied text itself. | ||
| 141 | # | ||
| 142 | # A one-tile wall, because that is where the geometry is knowable: `mux | ||
| 143 | # TARGET` is a wall of one tile whose rect is the whole terminal (no label | ||
| 144 | # bar), so a terminal row IS a grid row and the text can be put on one by | ||
| 145 | # name. The session is given its content with an explicit cursor address | ||
| 146 | # rather than by printing lines, so the row this drag crosses does not | ||
| 147 | # depend on where a prompt happened to leave the cursor. | ||
| 148 | # | ||
| 149 | # The typed line is `ZZ-%s` and the printed line is `ZZ-COPYME`, so the | ||
| 150 | # expect below cannot be satisfied by the shell ECHOING the command back. | ||
| 151 | # | ||
| 152 | # Columns 1 to 9 and no further: the prompt lands at column 10 on the same | ||
| 153 | # row, and a selection that reached it would copy the prompt too. | ||
| 154 | set +e | ||
| 155 | timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.selcap" --err "$OUT.selcap.err" -- \ | ||
| 156 | "$MUX" --sock "$SOCK50" --session s > "$OUT.selpc" 2>&1 <<'EOF' | ||
| 157 | settle 900 20000 | ||
| 158 | send printf '\\033[2J\\033[9;1HZZ-%s' COPYME\n | ||
| 159 | expect ZZ-COPYME 15000 | ||
| 160 | settle 700 20000 | ||
| 161 | send \x1b[<0;3;9M\x1b[<0;3;9m | ||
| 162 | settle 500 15000 | ||
| 163 | send \x1b[<0;1;9M\x1b[<32;9;9M\x1b[<0;9;9m | ||
| 164 | expect \x1b]52;c;WlotQ09QWU1F 15000 | ||
| 165 | send \x1cd | ||
| 166 | waitexit 10000 | ||
| 167 | EOF | ||
| 168 | RC=$? | ||
| 169 | set -e | ||
| 170 | [ "$RC" -eq 0 ] || { | ||
| 171 | echo "e2e FAIL: drag copy: ptyclient leg exited $RC (no OSC 52 with the dragged text?):" | ||
| 172 | cat "$OUT.selpc"; exit 1; } | ||
| 173 | # Exactly one copy on that tty, which is what makes the CLICK above a | ||
| 174 | # negative rather than decoration: a click highlights nothing, so it has | ||
| 175 | # nothing to copy, and a client that copied on every button release would | ||
| 176 | # have written two. | ||
| 177 | SELCOPIES=$(grep -ao "$(printf '\033]52;')" "$OUT.selcap" | wc -l) | ||
| 178 | [ "$SELCOPIES" = "1" ] || { | ||
| 179 | echo "e2e FAIL: drag copy: $SELCOPIES OSC 52 writes on the client's tty, expected 1"; exit 1; } | ||
| 180 | # ---- a drag over WIDE cells leaves the client's screen converged ------- | ||
| 181 | # | ||
| 182 | # The gate the wide-cell shift walked straight through. Every other | ||
| 183 | # assertion this feature has is either on the daemon's state or on the | ||
| 184 | # ESCAPE BYTES the painter emitted — and a row that emits the same wide | ||
| 185 | # glyph TWICE still contains every substring the painter is supposed to | ||
| 186 | # emit, while landing a column wider than the grid. Only a check that | ||
| 187 | # judges the client's screen against the daemon's own, through a separate | ||
| 188 | # engine, can see that: which is assert_converged, and which no selection | ||
| 189 | # leg used until this one. | ||
| 190 | # | ||
| 191 | # Both edges of the drag land inside a wide cell on purpose. Row 3 is | ||
| 192 | # `w漢字x-WIDEMARK`, so column 3 is the spacer tail of 漢 and column 4 the | ||
| 193 | # first half of 字 — the two ways a span can cut a wide cell in half. | ||
| 194 | # | ||
| 195 | # The DEFAULT session, unlike the copy leg above: `converged_quiet` dumps | ||
| 196 | # the daemon's default grid, so a leg that named a session would diff this | ||
| 197 | # client's screen against a grid it never attached to and fail for a reason | ||
| 198 | # that has nothing to do with wide cells. | ||
| 199 | # | ||
| 200 | # Asserted to fail, per the wan.sh rule: with `snapWide` stubbed out to | ||
| 201 | # return its arguments, this leg renders `w 漢字x-WIDEMARK` against the | ||
| 202 | # daemon's `w漢字x-WIDEMARK` — the extra column, which is the bug. | ||
| 203 | # | ||
| 204 | # The click after the drag is what makes the STYLED half of this check | ||
| 205 | # mean something. A live highlight is reverse video the daemon has no | ||
| 206 | # notion of, so a leg that detached still holding one would diverge on | ||
| 207 | # style every time and could only ever assert the plain grid. Clearing it | ||
| 208 | # first buys the byte-exact comparison, and with it the assertion that a | ||
| 209 | # highlight leaves nothing behind when it goes — the overlay-never-becomes- | ||
| 210 | # state rule, on the only overlay that paints through the replica. | ||
| 211 | set +e | ||
| 212 | timeout 60 "$PTYCLIENT" --cols 40 --rows 12 --out "$OUT.swcap" --err "$OUT.swcap.err" -- \ | ||
| 213 | "$MUX" --sock "$SOCK50" > "$OUT.swpc" 2>&1 <<'EOF' | ||
| 214 | settle 900 20000 | ||
| 215 | send printf '\\033[2J\\033[3;1Hw\\346\\274\\242\\345\\255\\227x-WIDEMARK'\n | ||
| 216 | expect WIDEMARK 15000 | ||
| 217 | settle 700 20000 | ||
| 218 | send \x1b[<0;3;3M\x1b[<32;4;3M\x1b[<0;4;3m | ||
| 219 | settle 700 20000 | ||
| 220 | send \x1b[<0;1;3M\x1b[<0;1;3m | ||
| 221 | settle 700 20000 | ||
| 222 | send \x1cd | ||
| 223 | waitexit 10000 | ||
| 224 | EOF | ||
| 225 | RC=$? | ||
| 226 | set -e | ||
| 227 | [ "$RC" -eq 0 ] || { | ||
| 228 | echo "e2e FAIL: wide drag: ptyclient leg exited $RC:" | ||
| 229 | # The client's own stderr as well as the fixture's: the fixture can only | ||
| 230 | # report THAT the client closed the pty, and the reason is over here. | ||
| 231 | cat "$OUT.swpc" "$OUT.swcap.err"; exit 1; } | ||
| 232 | assert_converged "$OUT.swcap" "$SOCK50" "wide drag" 40 12 | ||
| 233 | ok "a drag across wide cells leaves the client's screen converged" | ||
| 234 | |||
| 235 | assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop" | ||
| 236 | D48PID="" | ||
| 237 | ok "a drag copies on release, and a click copies nothing" | ||
| 238 | |||
| 239 | # The in-band resize leg: a daemon of its own, so the grid it reports on is | ||
| 240 | # the one this leg sized. | ||
| 241 | SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock" | ||
| 242 | defer_sock "$SOCK51" | ||
| 243 | |||
| 244 | # --- a resize is told in-band to an app that asked for it (mode 2048) ------ | ||
| 245 | # | ||
| 246 | # nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and | ||
| 247 | # the engine says "recognised" — so nvim turns the mode on and from then on | ||
| 248 | # IGNORES SIGWINCH, waiting for `CSI 48;rows;cols;h;w t` on its input. The | ||
| 249 | # daemon never sent one, and a resized session stayed painted at its old | ||
| 250 | # size until nvim was restarted (found by hand, 2026-08-22, a drag-resized | ||
| 251 | # window full of two overlaid screens). | ||
| 252 | # | ||
| 253 | # The witness needs no nvim: `cat -v` prints whatever arrives on its stdin, | ||
| 254 | # so the report — INPUT to the app — lands on the grid as text. Two reports | ||
| 255 | # are asserted, because they are two code paths: the one the mode-set | ||
| 256 | # itself owes (the app sizes itself from it), and the one each resize owes. | ||
| 257 | start_daemon "$SOCK51" "$OUT.inband.d" "in-band daemon never bound" --shell /bin/sh | ||
| 258 | D51PID=$DPID | ||
| 259 | set +e | ||
| 260 | timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.inband" --err "$OUT.inband.err" -- \ | ||
| 261 | "$MUX" --sock "$SOCK51" > "$OUT.inband.log" 2>&1 <<'EOF' | ||
| 262 | expect \x1b[?1049h 15000 | ||
| 263 | settle 400 15000 | ||
| 264 | send printf '\\033[?2048h'; cat -v\n | ||
| 265 | expect [48;30;100;0;0t 10000 | ||
| 266 | resize 120 40 | ||
| 267 | expect [48;40;120;0;0t 10000 | ||
| 268 | settle 500 15000 | ||
| 269 | send \x1c\x1c | ||
| 270 | waitexit 10000 | ||
| 271 | EOF | ||
| 272 | RC=$? | ||
| 273 | set -e | ||
| 274 | [ "$RC" -eq 0 ] || { | ||
| 275 | echo "e2e FAIL: in-band size: the leg exited $RC:"; cat "$OUT.inband.log"; exit 1; } | ||
| 276 | assert_stopped "$SOCK51" "$D51PID" "in-band size" "$OUT.inbstop" | ||
| 277 | D51PID="" | ||
| 278 | ok "a resize is reported in-band to an app that turned on mode 2048" | ||
| 279 | |||
| 280 | # --- The default socket path refuses to be guessed. Field incident: a tmux | ||
| 281 | # server started before logind exported XDG_RUNTIME_DIR handed every pane an | ||
| 282 | # environment without it, so a pane's `muxd stop` looked at the /tmp guess | ||
| 283 | # while the daemon a pane had started owned $XDG_RUNTIME_DIR/muxd.sock — one | ||
| 284 | # uid, one box, two daemons. `env -u` rather than the harness environment: | ||
| 285 | # on a box that HAS the variable the unset case is otherwise never taken. | ||
| 286 | # No convergence: nothing gets far enough to attach. | ||
| 287 | for BIN in "$MUXD stats" "$MUX"; do | ||
| 288 | # shellcheck disable=SC2086 # the two-word muxd form is deliberate | ||
| 289 | if env -u XDG_RUNTIME_DIR $BIN > "$OUT.nort" 2>&1; then | ||
| 290 | echo "e2e FAIL: '$BIN' with no XDG_RUNTIME_DIR did not refuse"; exit 1 | ||
| 291 | fi | ||
| 292 | grep -q 'XDG_RUNTIME_DIR is unset' "$OUT.nort" || { | ||
| 293 | echo "e2e FAIL: '$BIN' refusal does not name the variable:"; cat "$OUT.nort"; exit 1; } | ||
| 294 | grep -q -- '--sock' "$OUT.nort" || { | ||
| 295 | echo "e2e FAIL: '$BIN' refusal does not say how to fix it:"; cat "$OUT.nort"; exit 1; } | ||
| 296 | done | ||
| 297 | # muxa answers agents in JSON, so its refusal has to arrive as a reply. | ||
| 298 | env -u XDG_RUNTIME_DIR "$MUXA" status > "$OUT.nort.a" 2>&1 && { | ||
| 299 | echo "e2e FAIL: muxa with no XDG_RUNTIME_DIR did not refuse"; exit 1; } | ||
| 300 | grep -q '"error":"no default socket path"' "$OUT.nort.a" || { | ||
| 301 | echo "e2e FAIL: muxa refusal is not a JSON error:"; cat "$OUT.nort.a"; exit 1; } | ||
| 302 | # The socketless verbs must survive the same environment: --version is how an | ||
| 303 | # operator checks whether the scp landed, and it owes nothing to a socket. | ||
| 304 | env -u XDG_RUNTIME_DIR "$MUXD" --version | grep -q '^muxd 0\.' || { | ||
| 305 | echo "e2e FAIL: muxd --version needs XDG_RUNTIME_DIR"; exit 1; } | ||
| 306 | env -u XDG_RUNTIME_DIR "$MUX" --version | grep -q '^mux 0\.' || { | ||
| 307 | echo "e2e FAIL: mux --version needs XDG_RUNTIME_DIR"; exit 1; } | ||
| 308 | rm -f "$OUT.nort" "$OUT.nort.a" | ||
| 309 | ok "no XDG_RUNTIME_DIR: the default path is refused by name, --version is not" | ||
| 310 | |||
| 311 | # --- an offerer that cannot answer is hung up on, and ssh falls through ---- | ||
| 312 | # | ||
| 313 | # An agent_offer is a declaration, not a capability (decisions.md | ||
| 314 | # 2026-08-22). The offerer here is a real `mux -A` whose agent is a real | ||
| 315 | # ssh-agent under SIGSTOP: the kernel still completes the connect from the | ||
| 316 | # listen backlog, so the -A preflight — which fails OPEN on silence, by | ||
| 317 | # design — lets the client through, and every request the session forwards | ||
| 318 | # to it then vanishes. That is the mute peer the daemon's answer clock | ||
| 319 | # exists for, produced with nothing but the binaries this suite already | ||
| 320 | # requires. | ||
| 321 | # | ||
| 322 | # Measured by hand for the issue: ssh-add against a socket that accepts and | ||
| 323 | # never replies blocks past 8s, against one that accepts and closes it | ||
| 324 | # fails in 2ms. So the two things asserted are the two halves of the fix: | ||
| 325 | # the FIRST ssh-add comes back inside `timeout 8` (the daemon closed the | ||
| 326 | # channel for the client that would not), and the SECOND is refused in | ||
| 327 | # milliseconds (the offer was taken away, so nobody routes to the mute | ||
| 328 | # client again and ssh pays nothing). | ||
| 329 | start_daemon "$SOCK48" "$OUT.agtmute.d" "agent-mute daemon never bound" --shell /bin/sh | ||
| 330 | D42PID=$DPID | ||
| 331 | ssh-agent -a "$AGENT48" > "$OUT.agtmute.env" 2>&1 | ||
| 332 | AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agtmute.env") | ||
| 333 | defer_kill "$AGENT48PID" | ||
| 334 | [ -n "$AGENT48PID" ] || { | ||
| 335 | echo "e2e FAIL: agent-mute: ssh-agent printed no pid to stop:" | ||
| 336 | cat "$OUT.agtmute.env"; exit 1; } | ||
| 337 | kill -STOP "$AGENT48PID" | ||
| 338 | set +e | ||
| 339 | SSH_AUTH_SOCK="$AGENT48" timeout 60 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 340 | --out "$OUT.agtmute" --err "$OUT.agtmute.err" \ | ||
| 341 | -- "$MUX" -A --sock "$SOCK48" > "$OUT.agtmute.log" 2>&1 <<'EOF' | ||
| 342 | expect \x1b[?1049h 15000 | ||
| 343 | settle 400 15000 | ||
| 344 | send timeout 8 ssh-add -l; echo mu""te1=$?\n | ||
| 345 | expect mute1= 20000 | ||
| 346 | send S=$(date +%s%N); ssh-add -l; R=$?; echo mu""te2=$R ms=$(( ($(date +%s%N)-S)/1000000 ))\n | ||
| 347 | expect mute2= 15000 | ||
| 348 | settle 400 15000 | ||
| 349 | send exit\n | ||
| 350 | waitexit 10000 | ||
| 351 | EOF | ||
| 352 | RC=$? | ||
| 353 | set -e | ||
| 354 | # The agent is resumed before it is killed: a stopped process ignores | ||
| 355 | # SIGTERM until it runs again, and the trap's plain kill would leave it. | ||
| 356 | kill -CONT "$AGENT48PID" 2>/dev/null || true | ||
| 357 | softkill "$AGENT48PID" || true | ||
| 358 | AGENT48PID="" | ||
| 359 | # The needles are split in the typed lines (`mu""te1=`) so an expect can | ||
| 360 | # only be satisfied by the command's OUTPUT, never by the pty's echo of the | ||
| 361 | # command — with an 8s wait in play, matching the echo runs the script | ||
| 362 | # ahead into a shell that is still busy. The wedge is checked before the | ||
| 363 | # exit code, because a wedged second ssh-add also swallows the `exit`. | ||
| 364 | grep -q "mute1=124" "$OUT.agtmute" && { | ||
| 365 | echo "e2e FAIL: agent-mute: the first ssh-add wedged for the whole 8s — the" | ||
| 366 | echo " daemon never hung up on the client that would not answer:" | ||
| 367 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 368 | [ "$RC" -eq 0 ] || { | ||
| 369 | echo "e2e FAIL: agent-mute: the leg exited $RC:" | ||
| 370 | cat "$OUT.agtmute.log"; exit 1; } | ||
| 371 | grep -qE "mute1=[1-9]" "$OUT.agtmute" || { | ||
| 372 | echo "e2e FAIL: agent-mute: the first ssh-add did not fail at all — something" | ||
| 373 | echo " answered for a stopped agent:" | ||
| 374 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 375 | MUTE2MS=$(sed -n 's/.*mute2=[0-9]* ms=\([0-9]*\).*/\1/p' "$OUT.agtmute" | head -1) | ||
| 376 | [ -n "$MUTE2MS" ] || { | ||
| 377 | echo "e2e FAIL: agent-mute: the second ssh-add reported no timing:" | ||
| 378 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 379 | # Well under the 5s bound, not merely under 8s: a refusal is the dial being | ||
| 380 | # closed at accept, and the only way to take 5s here is to be routed to the | ||
| 381 | # mute client again, which means the offer was not cleared. | ||
| 382 | [ "$MUTE2MS" -lt 2000 ] || { | ||
| 383 | echo "e2e FAIL: agent-mute: the second ssh-add took ${MUTE2MS}ms — the mute" | ||
| 384 | echo " client kept its offer and ssh paid the bound again" | ||
| 385 | cat -v "$OUT.agtmute"; exit 1; } | ||
| 386 | wait_pid_gone "$D42PID" "agent-mute: the session ended and the daemon should follow" | ||
| 387 | D42PID="" | ||
| 388 | ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)" | ||