76778c7a
refactor: a daemon's declaration stands with the leg that starts it
a73x 2026-08-26 18:42
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -237,467 +237,6 @@ unset MUX_KEY_FILE | |||
| 237 | # asserting against the developer's keyring. The legs that want an agent | 237 | # asserting against the developer's keyring. The legs that want an agent |
| 238 | # export one per command, at a path this file made. | 238 | # export one per command, at a path this file made. |
| 239 | unset SSH_AUTH_SOCK | 239 | unset SSH_AUTH_SOCK |
| 240 | # Second daemon, used only by the M7 abort scenario. | ||
| 241 | SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock" | ||
| 242 | defer_sock "$SOCK2" | ||
| 243 | # Third daemon, for the restart scenario: it gets killed and started again on | ||
| 244 | # the same path, so it cannot share the long-lived one. | ||
| 245 | SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock" | ||
| 246 | defer_sock "$SOCK3" | ||
| 247 | # Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP | ||
| 248 | # port, so it gets its own path rather than sharing the long-lived one. | ||
| 249 | SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock" | ||
| 250 | defer_sock "$SOCK4" "$SOCK4.nokey" "$SOCK4.second" | ||
| 251 | QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$" | ||
| 252 | defer_rm "$QKEY" "$QKEY.wrong" "$QKEY.bad" | ||
| 253 | # A port out of the way of the ephemeral range, made per-run so two suites can | ||
| 254 | # overlap. Collisions surface as a loud bind failure, never as a silent pass. | ||
| 255 | QPORT=$(( 21000 + ($$ % 4000) )) | ||
| 256 | # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it | ||
| 257 | # controls, so they cannot share the long-lived /bin/sh daemon. | ||
| 258 | SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock" | ||
| 259 | defer_sock "$SOCK5" | ||
| 260 | SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock" | ||
| 261 | defer_sock "$SOCK6" | ||
| 262 | SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock" | ||
| 263 | defer_sock "$SOCK7" | ||
| 264 | PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh" | ||
| 265 | defer_rm "$PWSH" | ||
| 266 | # M10 key-resolution daemons. Two, not one: the first scenario's client | ||
| 267 | # detaches and its daemon is killed, and the second must not be sharing | ||
| 268 | # either. Ports in bands of their own so a concurrent suite cannot collide. | ||
| 269 | SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock" | ||
| 270 | defer_sock "$SOCK9" | ||
| 271 | SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock" | ||
| 272 | defer_sock "$SOCK10" | ||
| 273 | QPORT2=$(( 26000 + ($$ % 4000) )) | ||
| 274 | QPORT3=$(( 31000 + ($$ % 4000) )) | ||
| 275 | # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never | ||
| 276 | # holds their pids as shell jobs — it reads them off the up-line and kills | ||
| 277 | # by that tracked pid, never by name. | ||
| 278 | SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock" | ||
| 279 | defer_sock "$SOCK8" | ||
| 280 | SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock" | ||
| 281 | defer_sock "$SOCK8T" | ||
| 282 | SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock" | ||
| 283 | defer_sock "$SOCK11" | ||
| 284 | QPORT4=$(( 36000 + ($$ % 4000) )) | ||
| 285 | # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2 | ||
| 286 | # resizes the grid twice and tp1's session is a scrollback-generating | ||
| 287 | # wrapper, so neither can share the long-lived /bin/sh daemon. | ||
| 288 | SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock" | ||
| 289 | defer_sock "$SOCK12" | ||
| 290 | SOCK13="${TMPDIR:-/tmp}/muxd-e2e-tp1-$$.sock" | ||
| 291 | defer_sock "$SOCK13" | ||
| 292 | # tp1's session: a wrapper that fills the scrollback and then becomes cat. | ||
| 293 | TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh" | ||
| 294 | defer_rm "$TP1SH" | ||
| 295 | # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the | ||
| 296 | # script is mid-flight — so its pid is tracked and killed like a daemon's. | ||
| 297 | # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY | ||
| 298 | # the proxy, the pty leg's by local mux under the M12 fixture. Both are | ||
| 299 | # torn down by `muxd stop` — the verb under test is also the cleanup, and | ||
| 300 | # the trap only backstops it. Neither daemon is a child of this shell, so | ||
| 301 | # each pid is read off the up-line its own spawner printed, which is the | ||
| 302 | # only handle that can never name a bystander. | ||
| 303 | SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock" | ||
| 304 | defer_sock "$SOCK14" | ||
| 305 | SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock" | ||
| 306 | defer_sock "$SOCK15" | ||
| 307 | # M14 ssh→QUIC handoff. The shim that stands in for ssh (built where the | ||
| 308 | # scenarios run), and a RUNTIME DIR | ||
| 309 | # per daemon: the handoff dials the DEFAULT socket on the far side, and the | ||
| 310 | # far side is this box, so each scenario's default socket has to be one of | ||
| 311 | # ours rather than the operator's. Two daemons, no more — the one a cold | ||
| 312 | # attach auto-starts, and the key-mismatch one that cannot be shared | ||
| 313 | # because it holds a key nothing else on this box has. | ||
| 314 | SSHIM_DIR="${TMPDIR:-/tmp}/muxd-e2e-sshim-$$" | ||
| 315 | defer_rm "$SSHIM_DIR" | ||
| 316 | SSHIM_PIDLOG="$SSHIM_DIR/pids" | ||
| 317 | export SSHIM_PIDLOG | ||
| 318 | HRUN="${TMPDIR:-/tmp}/mux-e2e-hrun-$$" | ||
| 319 | defer_rm "$HRUN" | ||
| 320 | HRUN2="${TMPDIR:-/tmp}/mux-e2e-hrun2-$$" | ||
| 321 | defer_rm "$HRUN2" | ||
| 322 | SOCK16="$HRUN/muxd.sock" | ||
| 323 | SOCK17="$HRUN2/muxd.sock" | ||
| 324 | defer_sock "$SOCK16" "$SOCK17" | ||
| 325 | HKEY="${TMPDIR:-/tmp}/mux-e2e-hkey-$$" | ||
| 326 | defer_rm "$HKEY" | ||
| 327 | # A config home that is a FILE: `muxd endpoint` cannot create a key under | ||
| 328 | # it and cannot find one, which is the announce-none scenario's lever. | ||
| 329 | HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$" | ||
| 330 | defer_rm "$HCFGBAD" | ||
| 331 | # Bands of their own, like every other port here, so a concurrent suite | ||
| 332 | # cannot collide — and BELOW the ephemeral range (32768–60999 here), which | ||
| 333 | # for HDEADPORT is not housekeeping but the scenario's correctness. It is | ||
| 334 | # the port the poisoned cache points at, and the daemon it is poisoning | ||
| 335 | # against holds a KERNEL-ASSIGNED ephemeral port: land on that one and the | ||
| 336 | # "dead" port is live, the dial succeeds, and the self-heal scenario | ||
| 337 | # quietly asserts a heal that never had to happen. | ||
| 338 | HQPORT=$(( 11000 + ($$ % 4000) )) | ||
| 339 | HDEADPORT=$(( 16000 + ($$ % 4000) )) | ||
| 340 | # The pipe_mux client in flight, and its FIFO. Both are registered by | ||
| 341 | # pipe_mux itself; its `timeout` stays the primary guarantee and the trap | ||
| 342 | # is the backstop for a scenario that exits between pipe_mux and | ||
| 343 | # pipe_detach, leaving a client the shell would otherwise orphan. | ||
| 344 | # M-web. Three daemons (the passivity pin's, the hub-basic one, and the | ||
| 345 | # tear scenario's two incarnations share SOCK20), two hub processes, and | ||
| 346 | # a port band of its own following the 5000-spacing convention — INSIDE | ||
| 347 | # the ephemeral range, which is fine for ports we bind (only provably-DEAD | ||
| 348 | # ports must stay below 32768; see HDEADPORT above). | ||
| 349 | SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock" | ||
| 350 | defer_sock "$SOCK18" | ||
| 351 | SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock" | ||
| 352 | defer_sock "$SOCK19" | ||
| 353 | SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock" | ||
| 354 | defer_sock "$SOCK20" | ||
| 355 | WPORT=$(( 41000 + ($$ % 4000) )) | ||
| 356 | WPORT2=$(( 46000 + ($$ % 4000) )) | ||
| 357 | # M18. Two daemons — the CLI multi-session block's and the wall's — and one | ||
| 358 | # more hub on the next 5000-spaced port. Only two sockets for four-and-some | ||
| 359 | # sessions, which is the whole point of the milestone: sessions are named on | ||
| 360 | # one socket, not spread across one socket each. | ||
| 361 | SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock" | ||
| 362 | defer_sock "$SOCK21" | ||
| 363 | SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock" | ||
| 364 | defer_sock "$SOCK22" | ||
| 365 | WPORT3=$(( 51000 + ($$ % 4000) )) | ||
| 366 | # ...and a third, the only M18 daemon holding a UDP port: it serves the | ||
| 367 | # session socket AND a QUIC listener at once, which is what lets one block | ||
| 368 | # ask whether a session name means the same session on either transport. | ||
| 369 | # Next 5000-spaced band after WPORT3. | ||
| 370 | SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock" | ||
| 371 | defer_sock "$SOCK23" | ||
| 372 | QPORT5=$(( 56000 + ($$ % 4000) )) | ||
| 373 | # A key of its own rather than the M8 block's $QKEY: that one is rm_swept | ||
| 374 | # the moment the M8 scenarios finish (see the sweep after the key-source | ||
| 375 | # block), which is hundreds of lines before this one runs. Reaching for a | ||
| 376 | # deleted file would make this block's daemon fail to bind for a reason | ||
| 377 | # that has nothing to do with sessions. | ||
| 378 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" | ||
| 379 | defer_rm "$M18KEY" | ||
| 380 | |||
| 381 | # The bell leg's own daemon. A socket of its own is load-bearing rather than | ||
| 382 | # tidiness, and the argument is at the leg itself: that scenario counts BEL | ||
| 383 | # BYTES in a host capture, and a BEL is also what terminates an OSC. $SOCK's | ||
| 384 | # session has a window title set on it by the title leg, and every client | ||
| 385 | # attaching there is told so as `ESC]0;...BEL` — which would land in the | ||
| 386 | # capture and make the count assert something other than what it says. | ||
| 387 | SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock" | ||
| 388 | defer_sock "$SOCK24" | ||
| 389 | |||
| 390 | # The dynamic-wall leg: one daemon, one hub restarted three times, and a | ||
| 391 | # STATE HOME of its own on the 61000 band — the last 5000-spaced one that | ||
| 392 | # fits under 65535, the QUIC block's 56000 being the one before. The state | ||
| 393 | # home is the load-bearing part: this leg reads the wall file back as an | ||
| 394 | # artifact, and $XDG_STATE_HOME above is shared with every other scenario — | ||
| 395 | # the M-web and M18 hubs write their argv walls there, so a wall read out of | ||
| 396 | # it would be some other block's. Never the developer's ~/.local/state either; that is | ||
| 397 | # why every muxweb here is spawned with the override in front of it. | ||
| 398 | SOCK25="${TMPDIR:-/tmp}/muxd-e2e-dynwall-$$.sock" | ||
| 399 | defer_sock "$SOCK25" | ||
| 400 | WPORT4=$(( 61000 + ($$ % 4000) )) | ||
| 401 | DWSTATE="${TMPDIR:-/tmp}/mux-e2e-dynwall-state-$$" | ||
| 402 | defer_rm "$DWSTATE" | ||
| 403 | |||
| 404 | # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be | ||
| 405 | # confused with any other block's. | ||
| 406 | SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock" | ||
| 407 | defer_sock "$SOCK26" | ||
| 408 | |||
| 409 | # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord | ||
| 410 | # that fails to detach cannot strand another block's session. | ||
| 411 | SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" | ||
| 412 | defer_sock "$SOCK27" | ||
| 413 | |||
| 414 | # Ctrl-\ c (create a session and switch to it): its own daemon, because the | ||
| 415 | # assertion is about which sessions EXIST — another block's session on the | ||
| 416 | # same daemon would change the name the chord picks. | ||
| 417 | SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock" | ||
| 418 | defer_sock "$SOCK28" | ||
| 419 | |||
| 420 | # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same | ||
| 421 | # reason as SOCK28, and more sharply — the ring IS the list of sessions on | ||
| 422 | # one daemon, so a stray session from another block would change where a | ||
| 423 | # step lands. | ||
| 424 | SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" | ||
| 425 | defer_sock "$SOCK29" | ||
| 426 | |||
| 427 | # M5 (the self-attach refusal): its own daemon because the assertion reads | ||
| 428 | # the SOCKET PATH out of a session shell's environment and compares it to | ||
| 429 | # the one this block started — a shared daemon would make the comparison a | ||
| 430 | # tautology about whichever block bound first. | ||
| 431 | SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock" | ||
| 432 | defer_sock "$SOCK31" | ||
| 433 | |||
| 434 | # The wheel pair. Each needs a scrollback of its own to scroll (or to prove | ||
| 435 | # it did not), which the long-lived /bin/sh daemon has no way to hold still, | ||
| 436 | # and the second one needs a session that ASKS for the mouse — a mode that | ||
| 437 | # would follow every other scenario sharing the daemon around. | ||
| 438 | SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock" | ||
| 439 | defer_sock "$SOCK33" | ||
| 440 | WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh" | ||
| 441 | defer_rm "$WHEELSH" | ||
| 442 | SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock" | ||
| 443 | defer_sock "$SOCK34" | ||
| 444 | MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh" | ||
| 445 | defer_rm "$MOUSESH" | ||
| 446 | # The alternate-screen half of the wheel: its own daemon because the | ||
| 447 | # session under test is a PAGER holding the alt screen for its whole life, | ||
| 448 | # which no other scenario could share a grid with. And the no-terminal | ||
| 449 | # half, whose session is a bare `cat` so that what reaches the pty is | ||
| 450 | # echoed back into the grid the assertion reads. | ||
| 451 | SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock" | ||
| 452 | defer_sock "$SOCK35" | ||
| 453 | LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh" | ||
| 454 | defer_rm "$LESSSH" | ||
| 455 | LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt" | ||
| 456 | defer_rm "$LESSDATA" | ||
| 457 | SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock" | ||
| 458 | defer_sock "$SOCK36" | ||
| 459 | |||
| 460 | # The focus-skip leg, on a daemon of its own for the grid-size reason: it | ||
| 461 | # asserts on grid sizes and on how many clients a session ever had at once, | ||
| 462 | # and a session another block created — or another block's client sitting | ||
| 463 | # on this daemon — would make both numbers say nothing. | ||
| 464 | SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock" | ||
| 465 | defer_sock "$SOCK37" | ||
| 466 | # The dead tile. Its own daemon because the leg needs a session name that | ||
| 467 | # does NOT exist — the daemon refuses the attach — and any other block's | ||
| 468 | # daemon might have one by that name. | ||
| 469 | SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock" | ||
| 470 | defer_sock "$SOCK39" | ||
| 471 | |||
| 472 | # The wheel inside the focused tile. Two daemons for the wheel pair's own | ||
| 473 | # reason, turned up one notch: each needs a session whose SHELL is fixed for | ||
| 474 | # the session's whole life — one that scrolls and one that ASKS for the | ||
| 475 | # mouse — and a mode another scenario's session set would follow the wall's | ||
| 476 | # terminal around. Both use the daemon's DEFAULT session, so the wall | ||
| 477 | # spelling is a bare `--sock PATH` and nothing has to create anything. | ||
| 478 | SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock" | ||
| 479 | defer_sock "$SOCK41" | ||
| 480 | ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh" | ||
| 481 | defer_rm "$ZWHEELSH" | ||
| 482 | SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock" | ||
| 483 | defer_sock "$SOCK42" | ||
| 484 | ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh" | ||
| 485 | defer_rm "$ZMOUSESH" | ||
| 486 | |||
| 487 | # The scrollback-rect leg: a focused tile's history page must own only its | ||
| 488 | # sub-rect, so its daemon gets a socket of its own for the grid-size | ||
| 489 | # reason every wall leg has. | ||
| 490 | SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" | ||
| 491 | defer_sock "$SOCK53" | ||
| 492 | SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock" | ||
| 493 | defer_sock "$SOCK54" | ||
| 494 | SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock" | ||
| 495 | defer_sock "$SOCK55" | ||
| 496 | SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock" | ||
| 497 | defer_sock "$SOCK56" | ||
| 498 | SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock" | ||
| 499 | defer_sock "$SOCK57" | ||
| 500 | SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock" | ||
| 501 | defer_sock "$SOCK58" | ||
| 502 | SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock" | ||
| 503 | defer_sock "$SOCK59" | ||
| 504 | SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock" | ||
| 505 | defer_sock "$SOCK60" | ||
| 506 | SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" | ||
| 507 | defer_sock "$SOCK61" | ||
| 508 | SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock" | ||
| 509 | defer_sock "$SOCK62" | ||
| 510 | SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock" | ||
| 511 | defer_sock "$SOCK63" | ||
| 512 | # The hydrated-create leg. One daemon, and it serves QUIC as well as its | ||
| 513 | # socket: the leg's whole point is the LOCAL/remote split, so both halves | ||
| 514 | # have to be the same run of the same daemon or the comparison is between | ||
| 515 | # two rigs rather than between two kinds of line. Short socket name on | ||
| 516 | # purpose — the assertion reads a label bar back off a 70-column render. | ||
| 517 | SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock" | ||
| 518 | defer_sock "$SOCK64" | ||
| 519 | # Base 6000, below the 5000-spaced band the other QUIC ports share: every | ||
| 520 | # base from 11000 up is already taken, and a DUPLICATED base passes only | ||
| 521 | # for as long as the other leg's daemon happens to be dead by the time | ||
| 522 | # this one binds. A collision reads as this daemon's failure, not the | ||
| 523 | # suite's, which is why the base has to be the leg's own. | ||
| 524 | HYPORT=$(( 6000 + ($$ % 4000) )) | ||
| 525 | HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$" | ||
| 526 | defer_rm "$HYKEY" | ||
| 527 | HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$" | ||
| 528 | defer_rm "$HYSTATE" | ||
| 529 | HYWALL="$HYSTATE/mux/wall" | ||
| 530 | # The refused-attach leg: its own daemon, because its assertions are about | ||
| 531 | # what a daemon says to a name it does NOT have, and a socket shared with | ||
| 532 | # another leg would let that leg's sessions decide what "does not have" | ||
| 533 | # means. | ||
| 534 | SOCK66="${TMPDIR:-/tmp}/muxd-e2e-refuse-$$.sock" | ||
| 535 | defer_sock "$SOCK66" | ||
| 536 | REFSTATE="${TMPDIR:-/tmp}/mux-e2e-refuse-state-$$" | ||
| 537 | defer_rm "$REFSTATE" | ||
| 538 | # The browser half of the same ruling: one daemon serving a socket AND a | ||
| 539 | # QUIC listener, one hub, a state home of its own. Two ports, and the | ||
| 540 | # 5000-spaced bands are exhausted — 61000 is the last one that fits under | ||
| 541 | # 65535 — so this leg takes the tail ABOVE the ephemeral range | ||
| 542 | # (ip_local_port_range tops out at 60999 by default), halved. The narrow | ||
| 543 | # `%250` is the price of that tail: two runs whose pids differ by 250 | ||
| 544 | # collide where the older bands need 4000. A collision reads as this leg's | ||
| 545 | # daemon failing to bind, which is also why the two halves cannot share a | ||
| 546 | # base. | ||
| 547 | SOCK67="${TMPDIR:-/tmp}/muxd-e2e-wg-$$.sock" | ||
| 548 | defer_sock "$SOCK67" | ||
| 549 | WGPORT=$(( 65000 + ($$ % 250) )) | ||
| 550 | WGQPORT=$(( 65250 + ($$ % 250) )) | ||
| 551 | WGKEY="${TMPDIR:-/tmp}/mux-e2e-wgkey-$$" | ||
| 552 | defer_rm "$WGKEY" | ||
| 553 | WGSTATE="${TMPDIR:-/tmp}/mux-e2e-wg-state-$$" | ||
| 554 | defer_rm "$WGSTATE" | ||
| 555 | WGWALL="$WGSTATE/mux/wall" | ||
| 556 | # The refusal-spin leg. Its own everything: a shim dir on PATH, a state | ||
| 557 | # home holding the wall it restores, and a dial log the shim appends to. | ||
| 558 | SOCK68="${TMPDIR:-/tmp}/muxd-e2e-sp-$$.sock" | ||
| 559 | defer_sock "$SOCK68" | ||
| 560 | SPPORT=$(( 26000 + ($$ % 4000) )) | ||
| 561 | SPSTATE="${TMPDIR:-/tmp}/mux-e2e-sp-state-$$" | ||
| 562 | defer_rm "$SPSTATE" | ||
| 563 | SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$" | ||
| 564 | defer_rm "$SPDIR" | ||
| 565 | SPINLOG="$SPDIR/dials" | ||
| 566 | # `muxd upgrade` — four daemons, because each one is a different daemon | ||
| 567 | # LIFECYCLE and no two can share a process: the same-binary leg's daemon | ||
| 568 | # ends up running a second image, the rollback leg's is born with the abort | ||
| 569 | # armed in its environment, the agent leg's holds an `-A` client, and the | ||
| 570 | # QUIC leg's owns a UDP socket. They run one after another, so the last of | ||
| 571 | # them is the only leg here that needs a port. | ||
| 572 | SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock" | ||
| 573 | defer_sock "$SOCK69" | ||
| 574 | SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock" | ||
| 575 | defer_sock "$SOCK70" | ||
| 576 | SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock" | ||
| 577 | defer_sock "$SOCK71" | ||
| 578 | SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock" | ||
| 579 | defer_sock "$SOCK72" | ||
| 580 | SOCK73="${TMPDIR:-/tmp}/muxd-e2e-stopgone-$$.sock" | ||
| 581 | defer_sock "$SOCK73" | ||
| 582 | # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from | ||
| 583 | # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken | ||
| 584 | # for by the two browser ports. A collision here reads as this leg's daemon | ||
| 585 | # failing to bind, the same verdict every other band gives. | ||
| 586 | UPQPORT=$(( 2000 + ($$ % 4000) )) | ||
| 587 | UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$" | ||
| 588 | defer_rm "$UPKEY" | ||
| 589 | # A HOME of its own for the session shell. The title assertion below is | ||
| 590 | # about a title the SESSION set, and a distribution's rc file repaints the | ||
| 591 | # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says | ||
| 592 | # so of Arch's) — read out of the developer's own dotfiles, this leg would | ||
| 593 | # assert on whatever their prompt happens to spell. | ||
| 594 | UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$" | ||
| 595 | defer_rm "$UPHOME" | ||
| 596 | # The candidate binary the refusal leg offers: a copy, so its exec bit can | ||
| 597 | # be taken away without touching the one every other scenario runs. | ||
| 598 | UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$" | ||
| 599 | defer_rm "$UPBIN" | ||
| 600 | UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock" | ||
| 601 | defer_rm "$UPAGENT" | ||
| 602 | UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$" | ||
| 603 | defer_rm "$UPAGKEY" "$UPAGKEY.pub" | ||
| 604 | |||
| 605 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its | ||
| 606 | # own, for the dynamic-wall leg's reason turned up one notch: what these | ||
| 607 | # two blocks read back is the wall FILE, and every `mux` in this suite now | ||
| 608 | # writes a tile into the shared $XDG_STATE_HOME — so a file read out of | ||
| 609 | # that one would be every other block's attaches as much as this one's. | ||
| 610 | # The daemon is separate because the tile spelling contains the socket | ||
| 611 | # path, which is what the assertions grep for. | ||
| 612 | SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock" | ||
| 613 | defer_sock "$SOCK40" | ||
| 614 | WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$" | ||
| 615 | defer_rm "$WHSTATE" | ||
| 616 | WHWALL="$WHSTATE/mux/wall" | ||
| 617 | # A state home whose wall FILE is a directory: the unwritable case, which | ||
| 618 | # has to warn and let the attach happen anyway. | ||
| 619 | WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$" | ||
| 620 | defer_rm "$WHBAD" | ||
| 621 | # And a third, for the `x` block: its wall is built by two attaches and | ||
| 622 | # then eaten by `x`, so it must start empty and stay its own. | ||
| 623 | WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$" | ||
| 624 | defer_rm "$WHXSTATE" | ||
| 625 | |||
| 626 | # The convergence block: `mux TARGET` is a wall of one tile whose rect is | ||
| 627 | # the whole terminal. Three daemons and three state homes, each of them its own | ||
| 628 | # for the same reason the attach-history block's is — what these legs read | ||
| 629 | # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the | ||
| 630 | # suite has ever attached to. | ||
| 631 | SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock" | ||
| 632 | defer_sock "$SOCK45" | ||
| 633 | CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$" | ||
| 634 | defer_rm "$CVSTATE" | ||
| 635 | CVWALL="$CVSTATE/mux/wall" | ||
| 636 | # The ring-grows-the-wall leg needs TWO state homes: one for the client | ||
| 637 | # under test, and one for the setup client whose attach must NOT leave a | ||
| 638 | # tile the leg would then find already there. That absence is the case | ||
| 639 | # under test — a sibling the daemon knows and the wall does not. | ||
| 640 | SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock" | ||
| 641 | defer_sock "$SOCK46" | ||
| 642 | RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$" | ||
| 643 | defer_rm "$RGSTATE" | ||
| 644 | RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$" | ||
| 645 | defer_rm "$RGOTHER" | ||
| 646 | # The exit-semantics leg: a session ending in the focused tile. | ||
| 647 | SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" | ||
| 648 | defer_sock "$SOCK47" | ||
| 649 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" | ||
| 650 | defer_rm "$XESTATE" | ||
| 651 | |||
| 652 | # Agent forwarding: one socket path, two daemons in turn. The positive leg | ||
| 653 | # ends by typing `exit`, which ends that daemon's only session and takes the | ||
| 654 | # daemon with it — so the refusal leg below starts its own on the freed path | ||
| 655 | # rather than inheriting one that is already gone. Both are $MUXD; neither | ||
| 656 | # may be auto-started (see the $PATH note at the top of this file). | ||
| 657 | # | ||
| 658 | # The agent lives beside the sockets rather than under $OUT: a unix path is | ||
| 659 | # capped at 107 bytes by sun_path, and this one is dialled from inside a | ||
| 660 | # session shell, so the room it has is the room every other socket in this | ||
| 661 | # file was given. | ||
| 662 | SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock" | ||
| 663 | defer_sock "$SOCK48" | ||
| 664 | SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock" | ||
| 665 | defer_sock "$SOCK51" | ||
| 666 | SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock" | ||
| 667 | defer_sock "$SOCK52" | ||
| 668 | AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock" | ||
| 669 | defer_rm "$AGENT48" | ||
| 670 | AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$" | ||
| 671 | defer_rm "$AGENT48KEY" "$AGENT48KEY.pub" | ||
| 672 | D42PID="" | ||
| 673 | |||
| 674 | # The flip leg gets a daemon of its own, and not for isolation's sake: the | ||
| 675 | # one above is stopped by its own scenario's assert_stopped, so there is no | ||
| 676 | # daemon left to reuse. Two agents, two keys, two sockets — one agent behind | ||
| 677 | # both clients would print the same fingerprint whichever slot answered, and | ||
| 678 | # the leg would pass having proved nothing. Beside the sockets for the | ||
| 679 | # sun_path reason above; these are dialled from inside a session too. | ||
| 680 | SOCK49="${TMPDIR:-/tmp}/muxd-e2e-agentflip-$$.sock" | ||
| 681 | defer_sock "$SOCK49" | ||
| 682 | AGENT49A="${TMPDIR:-/tmp}/mux-e2e-agent2a-$$.sock" | ||
| 683 | defer_rm "$AGENT49A" | ||
| 684 | AGENT49B="${TMPDIR:-/tmp}/mux-e2e-agent2b-$$.sock" | ||
| 685 | defer_rm "$AGENT49B" | ||
| 686 | AGENT49APID="" | ||
| 687 | AGENT49BPID="" | ||
| 688 | AGENT49AKEY="${TMPDIR:-/tmp}/mux-e2e-agent2akey-$$" | ||
| 689 | defer_rm "$AGENT49AKEY" "$AGENT49AKEY.pub" | ||
| 690 | AGENT49BKEY="${TMPDIR:-/tmp}/mux-e2e-agent2bkey-$$" | ||
| 691 | defer_rm "$AGENT49BKEY" "$AGENT49BKEY.pub" | ||
| 692 | D43PID="" | ||
| 693 | FLIPAPID="" | ||
| 694 | |||
| 695 | # The wall's own mouse claim needs a daemon of its own for the same reason | ||
| 696 | # every other leg does: the wall scenario above stops its daemon on the way | ||
| 697 | # out, so there is nothing left to reuse. | ||
| 698 | SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock" | ||
| 699 | defer_sock "$SOCK50" | ||
| 700 | D48PID="" | ||
| 701 | 240 | ||
| 702 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 241 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 703 | # such line on exit; every field is a key=value pair, so a rename or reorder | 242 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| @@ -1616,6 +1155,42 @@ cleanup() { | |||
| 1616 | } | 1155 | } |
| 1617 | trap cleanup EXIT INT TERM | 1156 | trap cleanup EXIT INT TERM |
| 1618 | 1157 | ||
| 1158 | # Second daemon, used only by the M7 abort scenario. | ||
| 1159 | SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock" | ||
| 1160 | defer_sock "$SOCK2" | ||
| 1161 | # Third daemon, for the restart scenario: it gets killed and started again on | ||
| 1162 | # the same path, so it cannot share the long-lived one. | ||
| 1163 | SOCK3="${TMPDIR:-/tmp}/muxd-e2e-restart-$$.sock" | ||
| 1164 | defer_sock "$SOCK3" | ||
| 1165 | # Fourth daemon, for the M8 --quic scenario: it is the only one holding a UDP | ||
| 1166 | # port, so it gets its own path rather than sharing the long-lived one. | ||
| 1167 | SOCK4="${TMPDIR:-/tmp}/muxd-e2e-quic-$$.sock" | ||
| 1168 | defer_sock "$SOCK4" "$SOCK4.nokey" "$SOCK4.second" | ||
| 1169 | QKEY="${TMPDIR:-/tmp}/mux-e2e-key-$$" | ||
| 1170 | defer_rm "$QKEY" "$QKEY.wrong" "$QKEY.bad" | ||
| 1171 | # A port out of the way of the ephemeral range, made per-run so two suites can | ||
| 1172 | # overlap. Collisions surface as a loud bind failure, never as a silent pass. | ||
| 1173 | QPORT=$(( 21000 + ($$ % 4000) )) | ||
| 1174 | # M10 key-resolution daemons. Two, not one: the first scenario's client | ||
| 1175 | # detaches and its daemon is killed, and the second must not be sharing | ||
| 1176 | # either. Ports in bands of their own so a concurrent suite cannot collide. | ||
| 1177 | SOCK9="${TMPDIR:-/tmp}/muxd-e2e-envkey-$$.sock" | ||
| 1178 | defer_sock "$SOCK9" | ||
| 1179 | SOCK10="${TMPDIR:-/tmp}/muxd-e2e-flagwins-$$.sock" | ||
| 1180 | defer_sock "$SOCK10" | ||
| 1181 | QPORT2=$(( 26000 + ($$ % 4000) )) | ||
| 1182 | QPORT3=$(( 31000 + ($$ % 4000) )) | ||
| 1183 | # M10 `muxd start`. These daemons are spawned DETACHED, so the suite never | ||
| 1184 | # holds their pids as shell jobs — it reads them off the up-line and kills | ||
| 1185 | # by that tracked pid, never by name. | ||
| 1186 | SOCK8="${TMPDIR:-/tmp}/muxd-e2e-start-$$.sock" | ||
| 1187 | defer_sock "$SOCK8" | ||
| 1188 | SOCK8T="${TMPDIR:-/tmp}/muxd-e2e-trunc-$$.sock" | ||
| 1189 | defer_sock "$SOCK8T" | ||
| 1190 | SOCK11="${TMPDIR:-/tmp}/muxd-e2e-goal-$$.sock" | ||
| 1191 | defer_sock "$SOCK11" | ||
| 1192 | QPORT4=$(( 36000 + ($$ % 4000) )) | ||
| 1193 | |||
| 1619 | # --- M10: --version answers "did the scp land" without a daemon anywhere. | 1194 | # --- M10: --version answers "did the scp land" without a daemon anywhere. |
| 1620 | # No convergence: no daemon and no client, so there is no grid on either side. | 1195 | # No convergence: no daemon and no client, so there is no grid on either side. |
| 1621 | "$MUXD" --version | grep -q '^muxd 0\.' || { echo "e2e FAIL: muxd --version"; exit 1; } | 1196 | "$MUXD" --version | grep -q '^muxd 0\.' || { echo "e2e FAIL: muxd --version"; exit 1; } |
| @@ -2542,6 +2117,27 @@ softkill "$GPID" || true | |||
| 2542 | GPID="" | 2117 | GPID="" |
| 2543 | ok "keygen + start --quic + mux quic:// with no --key anywhere" | 2118 | ok "keygen + start --quic + mux quic:// with no --key anywhere" |
| 2544 | 2119 | ||
| 2120 | # M9 prediction: each scenario needs a session whose LINE DISCIPLINE it | ||
| 2121 | # controls, so they cannot share the long-lived /bin/sh daemon. | ||
| 2122 | SOCK5="${TMPDIR:-/tmp}/muxd-e2e-pred-$$.sock" | ||
| 2123 | defer_sock "$SOCK5" | ||
| 2124 | SOCK6="${TMPDIR:-/tmp}/muxd-e2e-pw-$$.sock" | ||
| 2125 | defer_sock "$SOCK6" | ||
| 2126 | SOCK7="${TMPDIR:-/tmp}/muxd-e2e-raw-$$.sock" | ||
| 2127 | defer_sock "$SOCK7" | ||
| 2128 | PWSH="${TMPDIR:-/tmp}/mux-e2e-pw-$$.sh" | ||
| 2129 | defer_rm "$PWSH" | ||
| 2130 | # M12 pty scenarios. Each needs a daemon whose grid size it owns: tp2 | ||
| 2131 | # resizes the grid twice and tp1's session is a scrollback-generating | ||
| 2132 | # wrapper, so neither can share the long-lived /bin/sh daemon. | ||
| 2133 | SOCK12="${TMPDIR:-/tmp}/muxd-e2e-tp2-$$.sock" | ||
| 2134 | defer_sock "$SOCK12" | ||
| 2135 | SOCK13="${TMPDIR:-/tmp}/muxd-e2e-tp1-$$.sock" | ||
| 2136 | defer_sock "$SOCK13" | ||
| 2137 | # tp1's session: a wrapper that fills the scrollback and then becomes cat. | ||
| 2138 | TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh" | ||
| 2139 | defer_rm "$TP1SH" | ||
| 2140 | |||
| 2545 | # ---- M9: prediction --------------------------------------------------- | 2141 | # ---- M9: prediction --------------------------------------------------- |
| 2546 | # | 2142 | # |
| 2547 | # Every scenario here asserts COUNTERS, because the screen cannot tell a | 2143 | # Every scenario here asserts COUNTERS, because the screen cannot tell a |
| @@ -3321,6 +2917,27 @@ D13PID="" | |||
| 3321 | rm_swept "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" "$OUT.tp1.d" "$TP1SH" | 2917 | rm_swept "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" "$OUT.tp1.d" "$TP1SH" |
| 3322 | ok "reconnect while scrolled: view restored, prediction resumed" | 2918 | ok "reconnect while scrolled: view restored, prediction resumed" |
| 3323 | 2919 | ||
| 2920 | # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the | ||
| 2921 | # script is mid-flight — so its pid is tracked and killed like a daemon's. | ||
| 2922 | # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY | ||
| 2923 | # the proxy, the pty leg's by local mux under the M12 fixture. Both are | ||
| 2924 | # torn down by `muxd stop` — the verb under test is also the cleanup, and | ||
| 2925 | # the trap only backstops it. Neither daemon is a child of this shell, so | ||
| 2926 | # each pid is read off the up-line its own spawner printed, which is the | ||
| 2927 | # only handle that can never name a bystander. | ||
| 2928 | SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock" | ||
| 2929 | defer_sock "$SOCK14" | ||
| 2930 | SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock" | ||
| 2931 | defer_sock "$SOCK15" | ||
| 2932 | # The bell leg's own daemon. A socket of its own is load-bearing rather than | ||
| 2933 | # tidiness, and the argument is at the leg itself: that scenario counts BEL | ||
| 2934 | # BYTES in a host capture, and a BEL is also what terminates an OSC. $SOCK's | ||
| 2935 | # session has a window title set on it by the title leg, and every client | ||
| 2936 | # attaching there is told so as `ESC]0;...BEL` — which would land in the | ||
| 2937 | # capture and make the count assert something other than what it says. | ||
| 2938 | SOCK24="${TMPDIR:-/tmp}/muxd-e2e-bell-$$.sock" | ||
| 2939 | defer_sock "$SOCK24" | ||
| 2940 | |||
| 3324 | # --- M13: attach auto-start (proxy) + muxd stop ------------------------ | 2941 | # --- M13: attach auto-start (proxy) + muxd stop ------------------------ |
| 3325 | # | 2942 | # |
| 3326 | # Nothing is serving SOCK14: the attach itself must produce the daemon. | 2943 | # Nothing is serving SOCK14: the attach itself must produce the daemon. |
| @@ -3854,6 +3471,53 @@ D21PID="" | |||
| 3854 | rm_swept "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" "$OUT.d21.d" | 3471 | rm_swept "$OUT.bell" "$OUT.bell.err" "$OUT.bell.log" "$OUT.bell.sh" "$OUT.d21.d" |
| 3855 | ok "a burst of bells in one chunk reaches the host as exactly one" | 3472 | ok "a burst of bells in one chunk reaches the host as exactly one" |
| 3856 | 3473 | ||
| 3474 | # The long-lived daemon has served every scenario that wanted it; stop it | ||
| 3475 | # NOW so its allocator verdict is written while the suite is still running | ||
| 3476 | # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain | ||
| 3477 | # (and the verdict) runs. | ||
| 3478 | # | ||
| 3479 | # The sweep that READS that verdict is in the EXIT trap, not here: this line | ||
| 3480 | # is only reached by a run that passed, and a leak deserves reporting on the | ||
| 3481 | # runs that did not (see leak_sweep). Everything below it is a pin, and the | ||
| 3482 | # trap fires after all of them either way. | ||
| 3483 | softkill "$D1PID" || true | ||
| 3484 | wait "$D1PID" 2>/dev/null || true | ||
| 3485 | D1PID="" | ||
| 3486 | |||
| 3487 | # M14 ssh→QUIC handoff. The shim that stands in for ssh (built where the | ||
| 3488 | # scenarios run), and a RUNTIME DIR | ||
| 3489 | # per daemon: the handoff dials the DEFAULT socket on the far side, and the | ||
| 3490 | # far side is this box, so each scenario's default socket has to be one of | ||
| 3491 | # ours rather than the operator's. Two daemons, no more — the one a cold | ||
| 3492 | # attach auto-starts, and the key-mismatch one that cannot be shared | ||
| 3493 | # because it holds a key nothing else on this box has. | ||
| 3494 | SSHIM_DIR="${TMPDIR:-/tmp}/muxd-e2e-sshim-$$" | ||
| 3495 | defer_rm "$SSHIM_DIR" | ||
| 3496 | SSHIM_PIDLOG="$SSHIM_DIR/pids" | ||
| 3497 | export SSHIM_PIDLOG | ||
| 3498 | HRUN="${TMPDIR:-/tmp}/mux-e2e-hrun-$$" | ||
| 3499 | defer_rm "$HRUN" | ||
| 3500 | HRUN2="${TMPDIR:-/tmp}/mux-e2e-hrun2-$$" | ||
| 3501 | defer_rm "$HRUN2" | ||
| 3502 | SOCK16="$HRUN/muxd.sock" | ||
| 3503 | SOCK17="$HRUN2/muxd.sock" | ||
| 3504 | defer_sock "$SOCK16" "$SOCK17" | ||
| 3505 | HKEY="${TMPDIR:-/tmp}/mux-e2e-hkey-$$" | ||
| 3506 | defer_rm "$HKEY" | ||
| 3507 | # A config home that is a FILE: `muxd endpoint` cannot create a key under | ||
| 3508 | # it and cannot find one, which is the announce-none scenario's lever. | ||
| 3509 | HCFGBAD="${TMPDIR:-/tmp}/mux-e2e-hnokey-$$" | ||
| 3510 | defer_rm "$HCFGBAD" | ||
| 3511 | # Bands of their own, like every other port here, so a concurrent suite | ||
| 3512 | # cannot collide — and BELOW the ephemeral range (32768–60999 here), which | ||
| 3513 | # for HDEADPORT is not housekeeping but the scenario's correctness. It is | ||
| 3514 | # the port the poisoned cache points at, and the daemon it is poisoning | ||
| 3515 | # against holds a KERNEL-ASSIGNED ephemeral port: land on that one and the | ||
| 3516 | # "dead" port is live, the dial succeeds, and the self-heal scenario | ||
| 3517 | # quietly asserts a heal that never had to happen. | ||
| 3518 | HQPORT=$(( 11000 + ($$ % 4000) )) | ||
| 3519 | HDEADPORT=$(( 16000 + ($$ % 4000) )) | ||
| 3520 | |||
| 3857 | # --- M14: the ssh→QUIC handoff ----------------------------------------- | 3521 | # --- M14: the ssh→QUIC handoff ----------------------------------------- |
| 3858 | # | 3522 | # |
| 3859 | # `mux HOST` fetches QUIC coordinates over ssh once, caches them, and | 3523 | # `mux HOST` fetches QUIC coordinates over ssh once, caches them, and |
| @@ -4259,6 +3923,30 @@ rm_swept "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err" | |||
| 4259 | "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD" | 3923 | "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD" |
| 4260 | rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" | 3924 | rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" |
| 4261 | 3925 | ||
| 3926 | # M18. Two daemons — the CLI multi-session block's and the wall's — and one | ||
| 3927 | # more hub on the next 5000-spaced port. Only two sockets for four-and-some | ||
| 3928 | # sessions, which is the whole point of the milestone: sessions are named on | ||
| 3929 | # one socket, not spread across one socket each. | ||
| 3930 | SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock" | ||
| 3931 | defer_sock "$SOCK21" | ||
| 3932 | SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock" | ||
| 3933 | defer_sock "$SOCK22" | ||
| 3934 | WPORT3=$(( 51000 + ($$ % 4000) )) | ||
| 3935 | # ...and a third, the only M18 daemon holding a UDP port: it serves the | ||
| 3936 | # session socket AND a QUIC listener at once, which is what lets one block | ||
| 3937 | # ask whether a session name means the same session on either transport. | ||
| 3938 | # Next 5000-spaced band after WPORT3. | ||
| 3939 | SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock" | ||
| 3940 | defer_sock "$SOCK23" | ||
| 3941 | QPORT5=$(( 56000 + ($$ % 4000) )) | ||
| 3942 | # A key of its own rather than the M8 block's $QKEY: that one is rm_swept | ||
| 3943 | # the moment the M8 scenarios finish (see the sweep after the key-source | ||
| 3944 | # block), which is hundreds of lines before this one runs. Reaching for a | ||
| 3945 | # deleted file would make this block's daemon fail to bind for a reason | ||
| 3946 | # that has nothing to do with sessions. | ||
| 3947 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" | ||
| 3948 | defer_rm "$M18KEY" | ||
| 3949 | |||
| 4262 | # --- M18: two sessions on one daemon are two shells --------------------- | 3950 | # --- M18: two sessions on one daemon are two shells --------------------- |
| 4263 | # The milestone, observed from outside: ONE daemon, one socket path, one | 3951 | # The milestone, observed from outside: ONE daemon, one socket path, one |
| 4264 | # key — and two sessions that share nothing. A connection IS a session | 3952 | # key — and two sessions that share nothing. A connection IS a session |
| @@ -4569,6 +4257,37 @@ assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop" | |||
| 4569 | D20PID="" | 4257 | D20PID="" |
| 4570 | ok "quic: two dials are two sessions, and one name is one session on either transport" | 4258 | ok "quic: two dials are two sessions, and one name is one session on either transport" |
| 4571 | 4259 | ||
| 4260 | # The pipe_mux client in flight, and its FIFO. Both are registered by | ||
| 4261 | # pipe_mux itself; its `timeout` stays the primary guarantee and the trap | ||
| 4262 | # is the backstop for a scenario that exits between pipe_mux and | ||
| 4263 | # pipe_detach, leaving a client the shell would otherwise orphan. | ||
| 4264 | # M-web. Three daemons (the passivity pin's, the hub-basic one, and the | ||
| 4265 | # tear scenario's two incarnations share SOCK20), two hub processes, and | ||
| 4266 | # a port band of its own following the 5000-spacing convention — INSIDE | ||
| 4267 | # the ephemeral range, which is fine for ports we bind (only provably-DEAD | ||
| 4268 | # ports must stay below 32768; see HDEADPORT above). | ||
| 4269 | SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock" | ||
| 4270 | defer_sock "$SOCK18" | ||
| 4271 | SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock" | ||
| 4272 | defer_sock "$SOCK19" | ||
| 4273 | SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock" | ||
| 4274 | defer_sock "$SOCK20" | ||
| 4275 | WPORT=$(( 41000 + ($$ % 4000) )) | ||
| 4276 | WPORT2=$(( 46000 + ($$ % 4000) )) | ||
| 4277 | # The dynamic-wall leg: one daemon, one hub restarted three times, and a | ||
| 4278 | # STATE HOME of its own on the 61000 band — the last 5000-spaced one that | ||
| 4279 | # fits under 65535, the QUIC block's 56000 being the one before. The state | ||
| 4280 | # home is the load-bearing part: this leg reads the wall file back as an | ||
| 4281 | # artifact, and $XDG_STATE_HOME above is shared with every other scenario — | ||
| 4282 | # the M-web and M18 hubs write their argv walls there, so a wall read out of | ||
| 4283 | # it would be some other block's. Never the developer's ~/.local/state either; that is | ||
| 4284 | # why every muxweb here is spawned with the override in front of it. | ||
| 4285 | SOCK25="${TMPDIR:-/tmp}/muxd-e2e-dynwall-$$.sock" | ||
| 4286 | defer_sock "$SOCK25" | ||
| 4287 | WPORT4=$(( 61000 + ($$ % 4000) )) | ||
| 4288 | DWSTATE="${TMPDIR:-/tmp}/mux-e2e-dynwall-state-$$" | ||
| 4289 | defer_rm "$DWSTATE" | ||
| 4290 | |||
| 4572 | # --- a 1x1 attach is refused the grid and can never claim it. | 4291 | # --- a 1x1 attach is refused the grid and can never claim it. |
| 4573 | # The degenerate size a client can genuinely arrive with — a terminal one | 4292 | # The degenerate size a client can genuinely arrive with — a terminal one |
| 4574 | # column wide — driven here on a real 1x1 pty. Nothing spells passivity | 4293 | # column wide — driven here on a real 1x1 pty. Nothing spells passivity |
| @@ -4962,6 +4681,32 @@ assert_stopped "$SOCK25" "$D22PID" "dyn wall" "$OUT.dwstop" | |||
| 4962 | D22PID="" | 4681 | D22PID="" |
| 4963 | ok "the wall is runtime state: add, remove, reorder, restore, argv adds" | 4682 | ok "the wall is runtime state: add, remove, reorder, restore, argv adds" |
| 4964 | 4683 | ||
| 4684 | # The CLI wall (`mux wall`): its own daemon, so its two sessions can't be | ||
| 4685 | # confused with any other block's. | ||
| 4686 | SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock" | ||
| 4687 | defer_sock "$SOCK26" | ||
| 4688 | # The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord | ||
| 4689 | # that fails to detach cannot strand another block's session. | ||
| 4690 | SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" | ||
| 4691 | defer_sock "$SOCK27" | ||
| 4692 | # Ctrl-\ c (create a session and switch to it): its own daemon, because the | ||
| 4693 | # assertion is about which sessions EXIST — another block's session on the | ||
| 4694 | # same daemon would change the name the chord picks. | ||
| 4695 | SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock" | ||
| 4696 | defer_sock "$SOCK28" | ||
| 4697 | # Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same | ||
| 4698 | # reason as SOCK28, and more sharply — the ring IS the list of sessions on | ||
| 4699 | # one daemon, so a stray session from another block would change where a | ||
| 4700 | # step lands. | ||
| 4701 | SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" | ||
| 4702 | defer_sock "$SOCK29" | ||
| 4703 | # M5 (the self-attach refusal): its own daemon because the assertion reads | ||
| 4704 | # the SOCKET PATH out of a session shell's environment and compares it to | ||
| 4705 | # the one this block started — a shared daemon would make the comparison a | ||
| 4706 | # tautology about whichever block bound first. | ||
| 4707 | SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock" | ||
| 4708 | defer_sock "$SOCK31" | ||
| 4709 | |||
| 4965 | # ---- the CLI wall: the hub's multiattach, in one terminal -------------- | 4710 | # ---- the CLI wall: the hub's multiattach, in one terminal -------------- |
| 4966 | # One daemon, two named sessions, one `mux wall` on a real pty showing | 4711 | # One daemon, two named sessions, one `mux wall` on a real pty showing |
| 4967 | # both AT ONCE — the browser wall's claim, made by the CLI. Three facts, | 4712 | # both AT ONCE — the browser wall's claim, made by the CLI. Three facts, |
| @@ -5387,6 +5132,57 @@ assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop" | |||
| 5387 | D28PID="" | 5132 | D28PID="" |
| 5388 | ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" | 5133 | ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" |
| 5389 | 5134 | ||
| 5135 | # The wheel pair. Each needs a scrollback of its own to scroll (or to prove | ||
| 5136 | # it did not), which the long-lived /bin/sh daemon has no way to hold still, | ||
| 5137 | # and the second one needs a session that ASKS for the mouse — a mode that | ||
| 5138 | # would follow every other scenario sharing the daemon around. | ||
| 5139 | SOCK33="${TMPDIR:-/tmp}/muxd-e2e-wheel-$$.sock" | ||
| 5140 | defer_sock "$SOCK33" | ||
| 5141 | WHEELSH="${TMPDIR:-/tmp}/mux-e2e-wheel-$$.sh" | ||
| 5142 | defer_rm "$WHEELSH" | ||
| 5143 | SOCK34="${TMPDIR:-/tmp}/muxd-e2e-appmouse-$$.sock" | ||
| 5144 | defer_sock "$SOCK34" | ||
| 5145 | MOUSESH="${TMPDIR:-/tmp}/mux-e2e-appmouse-$$.sh" | ||
| 5146 | defer_rm "$MOUSESH" | ||
| 5147 | # The alternate-screen half of the wheel: its own daemon because the | ||
| 5148 | # session under test is a PAGER holding the alt screen for its whole life, | ||
| 5149 | # which no other scenario could share a grid with. And the no-terminal | ||
| 5150 | # half, whose session is a bare `cat` so that what reaches the pty is | ||
| 5151 | # echoed back into the grid the assertion reads. | ||
| 5152 | SOCK35="${TMPDIR:-/tmp}/muxd-e2e-pager-$$.sock" | ||
| 5153 | defer_sock "$SOCK35" | ||
| 5154 | LESSSH="${TMPDIR:-/tmp}/mux-e2e-pager-$$.sh" | ||
| 5155 | defer_rm "$LESSSH" | ||
| 5156 | LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt" | ||
| 5157 | defer_rm "$LESSDATA" | ||
| 5158 | SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock" | ||
| 5159 | defer_sock "$SOCK36" | ||
| 5160 | # The focus-skip leg, on a daemon of its own for the grid-size reason: it | ||
| 5161 | # asserts on grid sizes and on how many clients a session ever had at once, | ||
| 5162 | # and a session another block created — or another block's client sitting | ||
| 5163 | # on this daemon — would make both numbers say nothing. | ||
| 5164 | SOCK37="${TMPDIR:-/tmp}/muxd-e2e-focusskip-$$.sock" | ||
| 5165 | defer_sock "$SOCK37" | ||
| 5166 | # The dead tile. Its own daemon because the leg needs a session name that | ||
| 5167 | # does NOT exist — the daemon refuses the attach — and any other block's | ||
| 5168 | # daemon might have one by that name. | ||
| 5169 | SOCK39="${TMPDIR:-/tmp}/muxd-e2e-deadtile-$$.sock" | ||
| 5170 | defer_sock "$SOCK39" | ||
| 5171 | # The wheel inside the focused tile. Two daemons for the wheel pair's own | ||
| 5172 | # reason, turned up one notch: each needs a session whose SHELL is fixed for | ||
| 5173 | # the session's whole life — one that scrolls and one that ASKS for the | ||
| 5174 | # mouse — and a mode another scenario's session set would follow the wall's | ||
| 5175 | # terminal around. Both use the daemon's DEFAULT session, so the wall | ||
| 5176 | # spelling is a bare `--sock PATH` and nothing has to create anything. | ||
| 5177 | SOCK41="${TMPDIR:-/tmp}/muxd-e2e-wallwheel-$$.sock" | ||
| 5178 | defer_sock "$SOCK41" | ||
| 5179 | ZWHEELSH="${TMPDIR:-/tmp}/mux-e2e-wallwheel-$$.sh" | ||
| 5180 | defer_rm "$ZWHEELSH" | ||
| 5181 | SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock" | ||
| 5182 | defer_sock "$SOCK42" | ||
| 5183 | ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh" | ||
| 5184 | defer_rm "$ZMOUSESH" | ||
| 5185 | |||
| 5390 | # ---- the wheel scrolls back, and an app that asks gets it instead ------- | 5186 | # ---- the wheel scrolls back, and an app that asks gets it instead ------- |
| 5391 | # | 5187 | # |
| 5392 | # The pair is one claim seen from both sides, so the two legs are the same | 5188 | # The pair is one claim seen from both sides, so the two legs are the same |
| @@ -6000,6 +5796,52 @@ assert_stopped "$SOCK39" "$D36PID" "dead tile" "$OUT.zdstop" | |||
| 6000 | D36PID="" | 5796 | D36PID="" |
| 6001 | ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one" | 5797 | ok "a tile whose pump has died narrates on its bar and on its prompt, and the focus stays on the live one" |
| 6002 | 5798 | ||
| 5799 | # The wall as attach HISTORY (phase 2). A daemon AND a state home of its | ||
| 5800 | # own, for the dynamic-wall leg's reason turned up one notch: what these | ||
| 5801 | # two blocks read back is the wall FILE, and every `mux` in this suite now | ||
| 5802 | # writes a tile into the shared $XDG_STATE_HOME — so a file read out of | ||
| 5803 | # that one would be every other block's attaches as much as this one's. | ||
| 5804 | # The daemon is separate because the tile spelling contains the socket | ||
| 5805 | # path, which is what the assertions grep for. | ||
| 5806 | SOCK40="${TMPDIR:-/tmp}/muxd-e2e-wallhist-$$.sock" | ||
| 5807 | defer_sock "$SOCK40" | ||
| 5808 | WHSTATE="${TMPDIR:-/tmp}/mux-e2e-wallhist-state-$$" | ||
| 5809 | defer_rm "$WHSTATE" | ||
| 5810 | WHWALL="$WHSTATE/mux/wall" | ||
| 5811 | # A state home whose wall FILE is a directory: the unwritable case, which | ||
| 5812 | # has to warn and let the attach happen anyway. | ||
| 5813 | WHBAD="${TMPDIR:-/tmp}/mux-e2e-wallbad-$$" | ||
| 5814 | defer_rm "$WHBAD" | ||
| 5815 | # And a third, for the `x` block: its wall is built by two attaches and | ||
| 5816 | # then eaten by `x`, so it must start empty and stay its own. | ||
| 5817 | WHXSTATE="${TMPDIR:-/tmp}/mux-e2e-wallx-state-$$" | ||
| 5818 | defer_rm "$WHXSTATE" | ||
| 5819 | # The convergence block: `mux TARGET` is a wall of one tile whose rect is | ||
| 5820 | # the whole terminal. Three daemons and three state homes, each of them its own | ||
| 5821 | # for the same reason the attach-history block's is — what these legs read | ||
| 5822 | # back is the wall FILE, and the shared $XDG_STATE_HOME is every socket the | ||
| 5823 | # suite has ever attached to. | ||
| 5824 | SOCK45="${TMPDIR:-/tmp}/muxd-e2e-converge-$$.sock" | ||
| 5825 | defer_sock "$SOCK45" | ||
| 5826 | CVSTATE="${TMPDIR:-/tmp}/mux-e2e-converge-state-$$" | ||
| 5827 | defer_rm "$CVSTATE" | ||
| 5828 | CVWALL="$CVSTATE/mux/wall" | ||
| 5829 | # The ring-grows-the-wall leg needs TWO state homes: one for the client | ||
| 5830 | # under test, and one for the setup client whose attach must NOT leave a | ||
| 5831 | # tile the leg would then find already there. That absence is the case | ||
| 5832 | # under test — a sibling the daemon knows and the wall does not. | ||
| 5833 | SOCK46="${TMPDIR:-/tmp}/muxd-e2e-ringgrow-$$.sock" | ||
| 5834 | defer_sock "$SOCK46" | ||
| 5835 | RGSTATE="${TMPDIR:-/tmp}/mux-e2e-ringgrow-state-$$" | ||
| 5836 | defer_rm "$RGSTATE" | ||
| 5837 | RGOTHER="${TMPDIR:-/tmp}/mux-e2e-ringgrow-other-$$" | ||
| 5838 | defer_rm "$RGOTHER" | ||
| 5839 | # The exit-semantics leg: a session ending in the focused tile. | ||
| 5840 | SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" | ||
| 5841 | defer_sock "$SOCK47" | ||
| 5842 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" | ||
| 5843 | defer_rm "$XESTATE" | ||
| 5844 | |||
| 6003 | # ---- the wall is attach HISTORY ----------------------------------------- | 5845 | # ---- the wall is attach HISTORY ----------------------------------------- |
| 6004 | # | 5846 | # |
| 6005 | # The mechanical rule (wall-home-screen spec, phase 2): an attach that | 5847 | # The mechanical rule (wall-home-screen spec, phase 2): an attach that |
| @@ -6587,6 +6429,39 @@ assert_stopped "$SOCK47" "$D47PID" "exit semantics" "$OUT.xestop" | |||
| 6587 | D47PID="" | 6429 | D47PID="" |
| 6588 | ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour" | 6430 | ok "a session ending in the focused tile exits mux alone, and leaves the focus to a neighbour" |
| 6589 | 6431 | ||
| 6432 | # Agent forwarding: one socket path, two daemons in turn. The positive leg | ||
| 6433 | # ends by typing `exit`, which ends that daemon's only session and takes the | ||
| 6434 | # daemon with it — so the refusal leg below starts its own on the freed path | ||
| 6435 | # rather than inheriting one that is already gone. Both are $MUXD; neither | ||
| 6436 | # may be auto-started (see the $PATH note at the top of this file). | ||
| 6437 | # | ||
| 6438 | # The agent lives beside the sockets rather than under $OUT: a unix path is | ||
| 6439 | # capped at 107 bytes by sun_path, and this one is dialled from inside a | ||
| 6440 | # session shell, so the room it has is the room every other socket in this | ||
| 6441 | # file was given. | ||
| 6442 | SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock" | ||
| 6443 | defer_sock "$SOCK48" | ||
| 6444 | AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock" | ||
| 6445 | defer_rm "$AGENT48" | ||
| 6446 | AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$" | ||
| 6447 | defer_rm "$AGENT48KEY" "$AGENT48KEY.pub" | ||
| 6448 | # The flip leg gets a daemon of its own, and not for isolation's sake: the | ||
| 6449 | # one above is stopped by its own scenario's assert_stopped, so there is no | ||
| 6450 | # daemon left to reuse. Two agents, two keys, two sockets — one agent behind | ||
| 6451 | # both clients would print the same fingerprint whichever slot answered, and | ||
| 6452 | # the leg would pass having proved nothing. Beside the sockets for the | ||
| 6453 | # sun_path reason above; these are dialled from inside a session too. | ||
| 6454 | SOCK49="${TMPDIR:-/tmp}/muxd-e2e-agentflip-$$.sock" | ||
| 6455 | defer_sock "$SOCK49" | ||
| 6456 | AGENT49A="${TMPDIR:-/tmp}/mux-e2e-agent2a-$$.sock" | ||
| 6457 | defer_rm "$AGENT49A" | ||
| 6458 | AGENT49B="${TMPDIR:-/tmp}/mux-e2e-agent2b-$$.sock" | ||
| 6459 | defer_rm "$AGENT49B" | ||
| 6460 | AGENT49AKEY="${TMPDIR:-/tmp}/mux-e2e-agent2akey-$$" | ||
| 6461 | defer_rm "$AGENT49AKEY" "$AGENT49AKEY.pub" | ||
| 6462 | AGENT49BKEY="${TMPDIR:-/tmp}/mux-e2e-agent2bkey-$$" | ||
| 6463 | defer_rm "$AGENT49BKEY" "$AGENT49BKEY.pub" | ||
| 6464 | |||
| 6590 | # --- agent forwarding: a real key in the client's agent answers in the session | 6465 | # --- agent forwarding: a real key in the client's agent answers in the session |
| 6591 | # | 6466 | # |
| 6592 | # Everything under this heading is a real program doing its real job: a real | 6467 | # Everything under this heading is a real program doing its real job: a real |
| @@ -7012,6 +6887,12 @@ softkill "$AGENT49BPID" || true | |||
| 7012 | AGENT49BPID="" | 6887 | AGENT49BPID="" |
| 7013 | ok "agent forwarding: the agent that answers is whoever typed last" | 6888 | ok "agent forwarding: the agent that answers is whoever typed last" |
| 7014 | 6889 | ||
| 6890 | # The wall's own mouse claim needs a daemon of its own for the same reason | ||
| 6891 | # every other leg does: the wall scenario above stops its daemon on the way | ||
| 6892 | # out, so there is nothing left to reuse. | ||
| 6893 | SOCK50="${TMPDIR:-/tmp}/muxd-e2e-wallmouse-$$.sock" | ||
| 6894 | defer_sock "$SOCK50" | ||
| 6895 | |||
| 7015 | # ---- the SOCK50 cluster: a daemon, two named sessions ------------------ | 6896 | # ---- the SOCK50 cluster: a daemon, two named sessions ------------------ |
| 7016 | # | 6897 | # |
| 7017 | # A daemon and two named sessions (a, b) shared by the click leg below and | 6898 | # A daemon and two named sessions (a, b) shared by the click leg below and |
| @@ -7240,6 +7121,11 @@ assert_stopped "$SOCK50" "$D48PID" "wall mouse" "$OUT.wmstop" | |||
| 7240 | D48PID="" | 7121 | D48PID="" |
| 7241 | ok "a drag copies on release, and a click copies nothing" | 7122 | ok "a drag copies on release, and a click copies nothing" |
| 7242 | 7123 | ||
| 7124 | # The in-band resize leg: a daemon of its own, so the grid it reports on is | ||
| 7125 | # the one this leg sized. | ||
| 7126 | SOCK51="${TMPDIR:-/tmp}/muxd-e2e-inband-$$.sock" | ||
| 7127 | defer_sock "$SOCK51" | ||
| 7128 | |||
| 7243 | # --- a resize is told in-band to an app that asked for it (mode 2048) ------ | 7129 | # --- a resize is told in-band to an app that asked for it (mode 2048) ------ |
| 7244 | # | 7130 | # |
| 7245 | # nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and | 7131 | # nvim 0.11+ asks the terminal for in-band size reports (DECRQM 2048), and |
| @@ -7386,7 +7272,38 @@ wait_pid_gone "$D42PID" "agent-mute: the session ended and the daemon should fol | |||
| 7386 | D42PID="" | 7272 | D42PID="" |
| 7387 | ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)" | 7273 | ok "agent forwarding: a mute offerer is hung up on, then no longer offered (${MUTE2MS}ms)" |
| 7388 | 7274 | ||
| 7275 | # The scrollback-rect leg: a focused tile's history page must own only its | ||
| 7276 | # sub-rect, so its daemon gets a socket of its own for the grid-size | ||
| 7277 | # reason every wall leg has. | ||
| 7278 | SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" | ||
| 7279 | defer_sock "$SOCK53" | ||
| 7280 | SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock" | ||
| 7281 | defer_sock "$SOCK54" | ||
| 7282 | SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock" | ||
| 7283 | defer_sock "$SOCK55" | ||
| 7284 | SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock" | ||
| 7285 | defer_sock "$SOCK56" | ||
| 7286 | SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock" | ||
| 7287 | defer_sock "$SOCK57" | ||
| 7288 | SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock" | ||
| 7289 | defer_sock "$SOCK58" | ||
| 7290 | SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock" | ||
| 7291 | defer_sock "$SOCK59" | ||
| 7292 | SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock" | ||
| 7293 | defer_sock "$SOCK60" | ||
| 7294 | SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock" | ||
| 7295 | defer_sock "$SOCK61" | ||
| 7296 | SOCK62="${TMPDIR:-/tmp}/muxd-e2e-promptA-$$.sock" | ||
| 7297 | defer_sock "$SOCK62" | ||
| 7298 | SOCK63="${TMPDIR:-/tmp}/muxd-e2e-promptB-$$.sock" | ||
| 7299 | defer_sock "$SOCK63" | ||
| 7300 | |||
| 7389 | # --- the cursor rests in the focused tile, whoever painted last ---------- | 7301 | # --- the cursor rests in the focused tile, whoever painted last ---------- |
| 7302 | # The cursor-ownership leg: a daemon of its own, for the grid-size reason | ||
| 7303 | # every wall leg has — the assertion is about where the cursor RESTS, and a | ||
| 7304 | # session another block created would change the tile it rests in. | ||
| 7305 | SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock" | ||
| 7306 | defer_sock "$SOCK52" | ||
| 7390 | # | 7307 | # |
| 7391 | # Every painter ends by showing the cursor where its own stripe sits, so | 7308 | # Every painter ends by showing the cursor where its own stripe sits, so |
| 7392 | # without an owner the visible cursor lands on whichever pump painted last: | 7309 | # without an owner the visible cursor lands on whichever pump painted last: |
| @@ -7525,41 +7442,6 @@ assert_stopped "$SOCK53" "$D54PID" "scrollback-rect" "$OUT.sb2stop" | |||
| 7525 | D54PID="" | 7442 | D54PID="" |
| 7526 | ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's pin stays" | 7443 | ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's pin stays" |
| 7527 | 7444 | ||
| 7528 | # The long-lived daemon has served every scenario that wanted it; stop it | ||
| 7529 | # NOW so its allocator verdict is written while the suite is still running | ||
| 7530 | # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain | ||
| 7531 | # (and the verdict) runs. | ||
| 7532 | # | ||
| 7533 | # The sweep that READS that verdict is in the EXIT trap, not here: this line | ||
| 7534 | # is only reached by a run that passed, and a leak deserves reporting on the | ||
| 7535 | # runs that did not (see leak_sweep). Everything below it is a pin, and the | ||
| 7536 | # trap fires after all of them either way. | ||
| 7537 | softkill "$D1PID" || true | ||
| 7538 | wait "$D1PID" 2>/dev/null || true | ||
| 7539 | D1PID="" | ||
| 7540 | |||
| 7541 | # The pins. Literals, not variables set from counting something else — | ||
| 7542 | # "assert the literal, never the constant the code under test reads" | ||
| 7543 | # (decisions.md, M10). The two figures live at the END of this file and | ||
| 7544 | # nowhere else: a count repeated in prose is a count nothing gates, and | ||
| 7545 | # both of the ones that used to stand here had drifted away from the pins | ||
| 7546 | # they described. Adding a scenario means editing those two literals by | ||
| 7547 | # hand, on purpose. | ||
| 7548 | # | ||
| 7549 | # Why most legs carry no convergence point. `assert_converged` replays one | ||
| 7550 | # client capture into an engine and diffs it against one daemon grid, so it | ||
| 7551 | # can only speak where BOTH exist and the grid is the subject. It says | ||
| 7552 | # nothing about a side channel (OSC 52, a window title, pty modes, a | ||
| 7553 | # fingerprint ssh printed, bytes counted rather than read), about a file an | ||
| 7554 | # editor or the wall wrote, about an exit code or a status field, about | ||
| 7555 | # WHICH of two sessions a keystroke reached, about scrollback the live grid | ||
| 7556 | # does not hold, about the absence of bytes two agreeing empties would | ||
| 7557 | # agree on anyway, or about a leg that never attaches at all. It also does | ||
| 7558 | # not claim to reproduce a full-screen application's redraws byte for byte, | ||
| 7559 | # which is why the alternate-screen legs stay out. A leg that asserts on | ||
| 7560 | # one of those and adds a convergence point would be pinning a fact every | ||
| 7561 | # leg above already establishes. | ||
| 7562 | |||
| 7563 | # ---- fullscreen: f toggles, focus follows, restore ------------------- | 7445 | # ---- fullscreen: f toggles, focus follows, restore ------------------- |
| 7564 | # | 7446 | # |
| 7565 | # `f` is a layout op, not a mode: the focused tile takes the whole | 7447 | # `f` is a layout op, not a mode: the focused tile takes the whole |
| @@ -8191,6 +8073,61 @@ D62PID="" | |||
| 8191 | rm -rf "$LPDSTATE" | 8073 | rm -rf "$LPDSTATE" |
| 8192 | ok "a corrupted sidecar degrades silently to the default layout" | 8074 | ok "a corrupted sidecar degrades silently to the default layout" |
| 8193 | 8075 | ||
| 8076 | # The hydrated-create leg. One daemon, and it serves QUIC as well as its | ||
| 8077 | # socket: the leg's whole point is the LOCAL/remote split, so both halves | ||
| 8078 | # have to be the same run of the same daemon or the comparison is between | ||
| 8079 | # two rigs rather than between two kinds of line. Short socket name on | ||
| 8080 | # purpose — the assertion reads a label bar back off a 70-column render. | ||
| 8081 | SOCK64="${TMPDIR:-/tmp}/muxd-e2e-hyd-$$.sock" | ||
| 8082 | defer_sock "$SOCK64" | ||
| 8083 | # Base 6000, below the 5000-spaced band the other QUIC ports share: every | ||
| 8084 | # base from 11000 up is already taken, and a DUPLICATED base passes only | ||
| 8085 | # for as long as the other leg's daemon happens to be dead by the time | ||
| 8086 | # this one binds. A collision reads as this daemon's failure, not the | ||
| 8087 | # suite's, which is why the base has to be the leg's own. | ||
| 8088 | HYPORT=$(( 6000 + ($$ % 4000) )) | ||
| 8089 | HYKEY="${TMPDIR:-/tmp}/mux-e2e-hydkey-$$" | ||
| 8090 | defer_rm "$HYKEY" | ||
| 8091 | HYSTATE="${TMPDIR:-/tmp}/mux-e2e-hyd-state-$$" | ||
| 8092 | defer_rm "$HYSTATE" | ||
| 8093 | HYWALL="$HYSTATE/mux/wall" | ||
| 8094 | # The refused-attach leg: its own daemon, because its assertions are about | ||
| 8095 | # what a daemon says to a name it does NOT have, and a socket shared with | ||
| 8096 | # another leg would let that leg's sessions decide what "does not have" | ||
| 8097 | # means. | ||
| 8098 | SOCK66="${TMPDIR:-/tmp}/muxd-e2e-refuse-$$.sock" | ||
| 8099 | defer_sock "$SOCK66" | ||
| 8100 | REFSTATE="${TMPDIR:-/tmp}/mux-e2e-refuse-state-$$" | ||
| 8101 | defer_rm "$REFSTATE" | ||
| 8102 | # The browser half of the same ruling: one daemon serving a socket AND a | ||
| 8103 | # QUIC listener, one hub, a state home of its own. Two ports, and the | ||
| 8104 | # 5000-spaced bands are exhausted — 61000 is the last one that fits under | ||
| 8105 | # 65535 — so this leg takes the tail ABOVE the ephemeral range | ||
| 8106 | # (ip_local_port_range tops out at 60999 by default), halved. The narrow | ||
| 8107 | # `%250` is the price of that tail: two runs whose pids differ by 250 | ||
| 8108 | # collide where the older bands need 4000. A collision reads as this leg's | ||
| 8109 | # daemon failing to bind, which is also why the two halves cannot share a | ||
| 8110 | # base. | ||
| 8111 | SOCK67="${TMPDIR:-/tmp}/muxd-e2e-wg-$$.sock" | ||
| 8112 | defer_sock "$SOCK67" | ||
| 8113 | WGPORT=$(( 65000 + ($$ % 250) )) | ||
| 8114 | WGQPORT=$(( 65250 + ($$ % 250) )) | ||
| 8115 | WGKEY="${TMPDIR:-/tmp}/mux-e2e-wgkey-$$" | ||
| 8116 | defer_rm "$WGKEY" | ||
| 8117 | WGSTATE="${TMPDIR:-/tmp}/mux-e2e-wg-state-$$" | ||
| 8118 | defer_rm "$WGSTATE" | ||
| 8119 | WGWALL="$WGSTATE/mux/wall" | ||
| 8120 | # The refusal-spin leg. Its own everything: a shim dir on PATH, a state | ||
| 8121 | # home holding the wall it restores, and a dial log the shim appends to. | ||
| 8122 | SOCK68="${TMPDIR:-/tmp}/muxd-e2e-sp-$$.sock" | ||
| 8123 | defer_sock "$SOCK68" | ||
| 8124 | SPPORT=$(( 26000 + ($$ % 4000) )) | ||
| 8125 | SPSTATE="${TMPDIR:-/tmp}/mux-e2e-sp-state-$$" | ||
| 8126 | defer_rm "$SPSTATE" | ||
| 8127 | SPDIR="${TMPDIR:-/tmp}/mux-e2e-sp-shim-$$" | ||
| 8128 | defer_rm "$SPDIR" | ||
| 8129 | SPINLOG="$SPDIR/dials" | ||
| 8130 | |||
| 8194 | # ---- Ctrl-\ : adds a tile by spelling --------------------------------- | 8131 | # ---- Ctrl-\ : adds a tile by spelling --------------------------------- |
| 8195 | # | 8132 | # |
| 8196 | # Argv typed from inside. A spelling naming a session on ANOTHER daemon | 8133 | # Argv typed from inside. A spelling naming a session on ANOTHER daemon |
| @@ -8843,6 +8780,44 @@ D68PID="" | |||
| 8843 | rm -rf "$SPSTATE" "$SPDIR" | 8780 | rm -rf "$SPSTATE" "$SPDIR" |
| 8844 | ok "a refusal the birth cannot fix backs off instead of spinning" | 8781 | ok "a refusal the birth cannot fix backs off instead of spinning" |
| 8845 | 8782 | ||
| 8783 | # `muxd upgrade` — four daemons, because each one is a different daemon | ||
| 8784 | # LIFECYCLE and no two can share a process: the same-binary leg's daemon | ||
| 8785 | # ends up running a second image, the rollback leg's is born with the abort | ||
| 8786 | # armed in its environment, the agent leg's holds an `-A` client, and the | ||
| 8787 | # QUIC leg's owns a UDP socket. They run one after another, so the last of | ||
| 8788 | # them is the only leg here that needs a port. | ||
| 8789 | SOCK69="${TMPDIR:-/tmp}/muxd-e2e-upgrade-$$.sock" | ||
| 8790 | defer_sock "$SOCK69" | ||
| 8791 | SOCK70="${TMPDIR:-/tmp}/muxd-e2e-uproll-$$.sock" | ||
| 8792 | defer_sock "$SOCK70" | ||
| 8793 | SOCK71="${TMPDIR:-/tmp}/muxd-e2e-upagent-$$.sock" | ||
| 8794 | defer_sock "$SOCK71" | ||
| 8795 | SOCK72="${TMPDIR:-/tmp}/muxd-e2e-upquic-$$.sock" | ||
| 8796 | defer_sock "$SOCK72" | ||
| 8797 | SOCK73="${TMPDIR:-/tmp}/muxd-e2e-stopgone-$$.sock" | ||
| 8798 | defer_sock "$SOCK73" | ||
| 8799 | # The next 4000-wide band DOWN from the hydrate leg's 6000: every base from | ||
| 8800 | # 11000 up is taken, and the 61000+ tail above the ephemeral range is spoken | ||
| 8801 | # for by the two browser ports. A collision here reads as this leg's daemon | ||
| 8802 | # failing to bind, the same verdict every other band gives. | ||
| 8803 | UPQPORT=$(( 2000 + ($$ % 4000) )) | ||
| 8804 | UPKEY="${TMPDIR:-/tmp}/mux-e2e-upkey-$$" | ||
| 8805 | defer_rm "$UPKEY" | ||
| 8806 | # A HOME of its own for the session shell. The title assertion below is | ||
| 8807 | # about a title the SESSION set, and a distribution's rc file repaints the | ||
| 8808 | # window title from PROMPT_COMMAND on every prompt (shellint.bash_init says | ||
| 8809 | # so of Arch's) — read out of the developer's own dotfiles, this leg would | ||
| 8810 | # assert on whatever their prompt happens to spell. | ||
| 8811 | UPHOME="${TMPDIR:-/tmp}/mux-e2e-uphome-$$" | ||
| 8812 | defer_rm "$UPHOME" | ||
| 8813 | # The candidate binary the refusal leg offers: a copy, so its exec bit can | ||
| 8814 | # be taken away without touching the one every other scenario runs. | ||
| 8815 | UPBIN="${TMPDIR:-/tmp}/mux-e2e-upcand-$$" | ||
| 8816 | defer_rm "$UPBIN" | ||
| 8817 | UPAGENT="${TMPDIR:-/tmp}/mux-e2e-upagent-$$.sock" | ||
| 8818 | defer_rm "$UPAGENT" | ||
| 8819 | UPAGKEY="${TMPDIR:-/tmp}/mux-e2e-upagkey-$$" | ||
| 8820 | defer_rm "$UPAGKEY" "$UPAGKEY.pub" | ||
| 8846 | 8821 | ||
| 8847 | # --- muxd upgrade: the daemon becomes the new binary, holding everything --- | 8822 | # --- muxd upgrade: the daemon becomes the new binary, holding everything --- |
| 8848 | # | 8823 | # |
| @@ -9377,6 +9352,50 @@ grep -q '^muxd: stopped' "$OUT.sg.stop" || { | |||
| 9377 | D73PID="" | 9352 | D73PID="" |
| 9378 | ok "muxd stop returns when the process is gone, not when the socket is" | 9353 | ok "muxd stop returns when the process is gone, not when the socket is" |
| 9379 | 9354 | ||
| 9355 | # The pins. Literals, not variables set from counting something else — | ||
| 9356 | # "assert the literal, never the constant the code under test reads" | ||
| 9357 | # (decisions.md, M10). The two figures live at the END of this file and | ||
| 9358 | # nowhere else: a count repeated in prose is a count nothing gates, and | ||
| 9359 | # both of the ones that used to stand here had drifted away from the pins | ||
| 9360 | # they described. Adding a scenario means editing those two literals by | ||
| 9361 | # hand, on purpose. | ||
| 9362 | # | ||
| 9363 | # Why most legs carry no convergence point. `assert_converged` replays one | ||
| 9364 | # client capture into an engine and diffs it against one daemon grid, so it | ||
| 9365 | # can only speak where BOTH exist and the grid is the subject. It says | ||
| 9366 | # nothing about a side channel (OSC 52, a window title, pty modes, a | ||
| 9367 | # fingerprint ssh printed, bytes counted rather than read), about a file an | ||
| 9368 | # editor or the wall wrote, about an exit code or a status field, about | ||
| 9369 | # WHICH of two sessions a keystroke reached, about scrollback the live grid | ||
| 9370 | # does not hold, about the absence of bytes two agreeing empties would | ||
| 9371 | # agree on anyway, or about a leg that never attaches at all. It also does | ||
| 9372 | # not claim to reproduce a full-screen application's redraws byte for byte, | ||
| 9373 | # which is why the alternate-screen legs stay out. A leg that asserts on | ||
| 9374 | # one of those and adds a convergence point would be pinning a fact every | ||
| 9375 | # leg above already establishes. | ||
| 9376 | |||
| 9377 | # The pins. Literals, not variables set from counting something else — | ||
| 9378 | # "assert the literal, never the constant the code under test reads" | ||
| 9379 | # (decisions.md, M10). The two figures live at the END of this file and | ||
| 9380 | # nowhere else: a count repeated in prose is a count nothing gates, and | ||
| 9381 | # both of the ones that used to stand here had drifted away from the pins | ||
| 9382 | # they described. Adding a scenario means editing those two literals by | ||
| 9383 | # hand, on purpose. | ||
| 9384 | # | ||
| 9385 | # Why most legs carry no convergence point. `assert_converged` replays one | ||
| 9386 | # client capture into an engine and diffs it against one daemon grid, so it | ||
| 9387 | # can only speak where BOTH exist and the grid is the subject. It says | ||
| 9388 | # nothing about a side channel (OSC 52, a window title, pty modes, a | ||
| 9389 | # fingerprint ssh printed, bytes counted rather than read), about a file an | ||
| 9390 | # editor or the wall wrote, about an exit code or a status field, about | ||
| 9391 | # WHICH of two sessions a keystroke reached, about scrollback the live grid | ||
| 9392 | # does not hold, about the absence of bytes two agreeing empties would | ||
| 9393 | # agree on anyway, or about a leg that never attaches at all. It also does | ||
| 9394 | # not claim to reproduce a full-screen application's redraws byte for byte, | ||
| 9395 | # which is why the alternate-screen legs stay out. A leg that asserts on | ||
| 9396 | # one of those and adds a convergence point would be pinning a fact every | ||
| 9397 | # leg above already establishes. | ||
| 9398 | |||
| 9380 | [ "$OK_COUNT" = "82" ] || { | 9399 | [ "$OK_COUNT" = "82" ] || { |
| 9381 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 82 —" | 9400 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 82 —" |
| 9382 | echo " a scenario was added (update the pin) or silently lost" | 9401 | echo " a scenario was added (update the pin) or silently lost" |