45b67e78
test: make mac drives a pristine macOS guest on the Mac the way make vm drives the VM
a73x 2026-09-04 10:16
Commit message
README.md
| Old | New | ||
|---|---|---|---|
| @@ -705,6 +705,28 @@ bell rings. The clipboard READ direction (`OSC 52` query) is refused | |||
| 705 | deliberately — answering it would let anything in any session read | 705 | deliberately — answering it would let anything in any session read |
| 706 | whatever you last copied. | 706 | whatever you last copied. |
| 707 | 707 | ||
| 708 | ## macOS | ||
| 709 | |||
| 710 | mux builds and runs on an Apple-silicon Mac. The gate for it is `make mac`, | ||
| 711 | which is the macOS half of what `make vm` is for Linux: it pushes this | ||
| 712 | commit to the Mac named by `MUX_MAC` (default `squirtle`), builds it there, | ||
| 713 | and then runs the user journeys against a *pristine* macOS virtual machine | ||
| 714 | with no developer tooling on it, so what is graded is the product on a bare | ||
| 715 | Mac rather than on the machine that compiled it. The VM is Cirrus Labs | ||
| 716 | `tart`, and the base image is made once by hand, on the Mac: | ||
| 717 | |||
| 718 | ```sh | ||
| 719 | brew trust cirruslabs/cli && brew install cirruslabs/cli/tart | ||
| 720 | tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest mux-mac-base | ||
| 721 | tart run --no-graphics mux-mac-base & # user admin, password admin | ||
| 722 | tart exec mux-mac-base sh -c 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "PUBKEY" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys' | ||
| 723 | tart stop mux-mac-base | ||
| 724 | ``` | ||
| 725 | |||
| 726 | `PUBKEY` is that Mac's own `~/.ssh/id_ed25519.pub`. Every `make mac` clones | ||
| 727 | `mux-mac-base` to `mux-mac`, boots the clone, and stops it on the way out, | ||
| 728 | so no run can see the run before it. | ||
| 729 | |||
| 708 | ## How it works | 730 | ## How it works |
| 709 | 731 | ||
| 710 | Both ends run a real terminal engine. The daemon's is authoritative: shell | 732 | Both ends run a real terminal engine. The daemon's is authoritative: shell |
test/mac.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,420 @@ | |||
| 1 | #!/bin/sh | ||
| 2 | # test/mac.sh — the `make mac` gate: real user journeys on a real macOS. | ||
| 3 | # | ||
| 4 | # What `make vm` is to the Linux port, this is to the macOS one. The unit | ||
| 5 | # suite and the e2e suite grade the code; this grades the PRODUCT on the | ||
| 6 | # operating system it was ported to, spelled exactly as a user spells it. | ||
| 7 | # | ||
| 8 | # Two machines, and the split is the point. The HOST (a Mac, $MUX_MAC) | ||
| 9 | # holds the toolchain and the shadow SDK, so it builds. The GUEST is a | ||
| 10 | # macOS virtual machine cloned fresh for every run, with no developer | ||
| 11 | # tooling on it at all, and it only RUNS what the host built — which is the | ||
| 12 | # stronger claim: a bare Mac, not the machine that compiled the binary. | ||
| 13 | # | ||
| 14 | # The pristine guest is a human action, never harness code (test/vm.sh's | ||
| 15 | # rule). Tooling is Cirrus Labs `tart`; the base image is | ||
| 16 | # ghcr.io/cirruslabs/macos-tahoe-base (macOS 26). The base VM is made ONCE, | ||
| 17 | # by hand, on the host: | ||
| 18 | # | ||
| 19 | # brew trust cirruslabs/cli && brew install cirruslabs/cli/tart | ||
| 20 | # tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest mux-mac-base | ||
| 21 | # tart run --no-graphics mux-mac-base & # user admin, password admin | ||
| 22 | # tart exec mux-mac-base sh -c 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "PUBKEY" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys' | ||
| 23 | # tart stop mux-mac-base | ||
| 24 | # | ||
| 25 | # PUBKEY is the host's own ~/.ssh/id_ed25519.pub, so the host can ssh into | ||
| 26 | # the guest without a password; `tart exec` needs the guest agent the | ||
| 27 | # cirruslabs images ship. `mux-mac-base` is never RUN by this script: every | ||
| 28 | # run clones it to `mux-mac`, boots the clone, and stops the clone on the | ||
| 29 | # way out. Hermeticity comes from the clone, which is why the daemon leg | ||
| 30 | # can assert the guest holds no mux and no runtime directory before it | ||
| 31 | # starts one — a second run must not be able to see the first run's. | ||
| 32 | # | ||
| 33 | # Every claim about the guest is asserted by a separate ssh reading the | ||
| 34 | # guest through test/os_oracle.sh, never by trusting the daemon's own | ||
| 35 | # report over the connection under test ("ask the OS about the OS"). | ||
| 36 | set -eu | ||
| 37 | |||
| 38 | MAC="${MUX_MAC:-squirtle}" | ||
| 39 | # Relative to the HOST's $HOME, because that is how ssh's scp-like remote | ||
| 40 | # spelling (`squirtle:code/rad/mux`) resolves a path. | ||
| 41 | REPO="${MUX_MAC_REPO:-code/rad/mux}" | ||
| 42 | BRANCH=mac-journey | ||
| 43 | VM_BASE=mux-mac-base | ||
| 44 | VM_RUN=mux-mac | ||
| 45 | GUSER="admin" | ||
| 46 | # A LITERAL $HOME: this string is pasted into a command line the HOST's | ||
| 47 | # shell parses, so the expansion has to happen there and not here. | ||
| 48 | GKEY='$HOME/.ssh/id_ed25519' | ||
| 49 | |||
| 50 | # BatchMode so a broken auth fails in seconds instead of prompting a | ||
| 51 | # harness nobody sits in front of. The guest key is accept-anything because | ||
| 52 | # a freshly cloned VM has a host key nobody has ever seen and takes a new | ||
| 53 | # one on the next run; the guest is reachable only on the host's own | ||
| 54 | # virtual network, and the host hop above it is BatchMode-authenticated. | ||
| 55 | HOST_SSH="-o BatchMode=yes -o ConnectTimeout=10" | ||
| 56 | # LogLevel=ERROR because a throwaway known-hosts file makes ssh announce the | ||
| 57 | # new key on stderr every single time, and that line would be most of what a | ||
| 58 | # green run prints. | ||
| 59 | GUEST_SSH="-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=10 -i $GKEY" | ||
| 60 | |||
| 61 | # now_ms, and the file_mode/pid_* names the guest legs source for | ||
| 62 | # themselves. The harness spells time and paths through the oracle so this | ||
| 63 | # script would still answer if it were ever run FROM a Mac. | ||
| 64 | . "$(dirname "$0")/os_oracle.sh" | ||
| 65 | |||
| 66 | GIP="" # the guest's address, set by the vm leg | ||
| 67 | OK_COUNT=0 | ||
| 68 | PUSHED=no | ||
| 69 | |||
| 70 | ok() { | ||
| 71 | OK_COUNT=$((OK_COUNT + 1)) | ||
| 72 | echo "$1 ok${2:+ ($2)}" | ||
| 73 | } | ||
| 74 | |||
| 75 | fail() { | ||
| 76 | echo "mac FAIL: $1: $2" | ||
| 77 | exit 1 | ||
| 78 | } | ||
| 79 | |||
| 80 | # hssh SECONDS — run the script on stdin on the HOST, under /bin/sh. | ||
| 81 | # | ||
| 82 | # /bin/sh and not the host's login shell (zsh): `$VAR` holding several | ||
| 83 | # words does not word-split in zsh, so an options string pasted into a | ||
| 84 | # command line there arrives as ONE argument and ssh answers "keyword | ||
| 85 | # batchmode extra arguments at end of line". | ||
| 86 | # | ||
| 87 | # The rule for a script handed to this: any ssh INSIDE it takes -n. The | ||
| 88 | # host's sh is reading its own script off stdin, and an ssh that does not | ||
| 89 | # close its stdin swallows the rest of that script — silently, with a 0 | ||
| 90 | # exit, so the lines after it simply never run. | ||
| 91 | hssh() { | ||
| 92 | _ht=$1 | ||
| 93 | # shellcheck disable=SC2086 # HOST_SSH is words on purpose | ||
| 94 | timeout "$_ht" ssh $HOST_SSH "$MAC" /bin/sh -s | ||
| 95 | } | ||
| 96 | |||
| 97 | # What every guest command runs under. XDG_STATE_HOME is isolated because | ||
| 98 | # every mux that attaches records its daemon in it (CLAUDE.md), and | ||
| 99 | # XDG_RUNTIME_DIR is UNSET on purpose: a fresh ssh login to a Mac has no | ||
| 100 | # such variable, and the Darwin fallback /tmp/mux-<uid> is exactly what the | ||
| 101 | # daemon leg is here to exercise end to end. | ||
| 102 | GUEST_PRELUDE=' | ||
| 103 | export XDG_STATE_HOME=$HOME/mux-e2e/state | ||
| 104 | unset XDG_RUNTIME_DIR | ||
| 105 | mkdir -p $HOME/mux-e2e | ||
| 106 | cd $HOME/mux-e2e | ||
| 107 | ' | ||
| 108 | |||
| 109 | # gssh SECONDS — run the script on stdin in the GUEST, through the host. | ||
| 110 | # | ||
| 111 | # The quoting rule is that there is NO quoting. The script rides this | ||
| 112 | # shell's stdin into the host's ssh, which forwards it to the guest's | ||
| 113 | # `sh -s`; no shell between here and there parses it as a command line, so | ||
| 114 | # a leg writes ordinary sh with whatever quotes it likes and nothing is | ||
| 115 | # escaped anywhere. The one thing a caller owes: always give this a | ||
| 116 | # heredoc. Called with no redirect it reads the enclosing script's stdin. | ||
| 117 | gssh() { | ||
| 118 | _gt=$1 | ||
| 119 | [ -n "$GIP" ] || fail vm "gssh before the guest had an address" | ||
| 120 | # shellcheck disable=SC2086 # HOST_SSH is words on purpose | ||
| 121 | { printf '%s\n' "$GUEST_PRELUDE"; cat; } | | ||
| 122 | timeout "$_gt" ssh $HOST_SSH "$MAC" "ssh $GUEST_SSH $GUSER@$GIP sh -s" | ||
| 123 | } | ||
| 124 | |||
| 125 | # The clone is stopped on every exit, including a failing one. It is left | ||
| 126 | # in place rather than deleted so a failure can be logged into, and the | ||
| 127 | # next run replaces it. | ||
| 128 | cleanup() { | ||
| 129 | _rc=$? | ||
| 130 | hssh 180 >/dev/null 2>&1 <<H || true | ||
| 131 | export PATH=/opt/homebrew/bin:\$PATH | ||
| 132 | tart stop $VM_RUN 2>/dev/null || true | ||
| 133 | H | ||
| 134 | exit "$_rc" | ||
| 135 | } | ||
| 136 | trap cleanup EXIT | ||
| 137 | |||
| 138 | # ---- leg 1: push — the host builds THIS commit ------------------------ | ||
| 139 | HEAD_SHA=$(git rev-parse HEAD) | ||
| 140 | HAVE=$(hssh 60 <<H || true | ||
| 141 | cd \$HOME/$REPO 2>/dev/null || exit 0 | ||
| 142 | git rev-parse -q --verify refs/heads/$BRANCH 2>/dev/null || true | ||
| 143 | H | ||
| 144 | ) | ||
| 145 | if [ "$HAVE" != "$HEAD_SHA" ]; then | ||
| 146 | # A push cannot update the branch the remote has checked out, and the | ||
| 147 | # run before this one left it checked out. Detaching first is what | ||
| 148 | # makes a re-run work; it changes no commit and no file. | ||
| 149 | hssh 60 >/dev/null <<H || fail push "cannot detach $MAC:$REPO before the push" | ||
| 150 | set -e | ||
| 151 | cd \$HOME/$REPO | ||
| 152 | if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then | ||
| 153 | git checkout -q --detach | ||
| 154 | fi | ||
| 155 | H | ||
| 156 | # --force because this branch is a scratch mirror of whatever this tree | ||
| 157 | # is and never shared history: a local amend or rebase leaves the host's | ||
| 158 | # ref on a commit the new HEAD does not descend from, and a plain push | ||
| 159 | # would be rejected as a non-fast-forward for the rest of the branch's | ||
| 160 | # life. | ||
| 161 | git push --force "$MAC:$REPO" "HEAD:refs/heads/$BRANCH" >/dev/null 2>&1 || | ||
| 162 | fail push "git push $MAC:$REPO HEAD:refs/heads/$BRANCH was refused" | ||
| 163 | PUSHED=yes | ||
| 164 | fi | ||
| 165 | hssh 60 >/dev/null <<H || fail push "the host could not check out $BRANCH" | ||
| 166 | set -e | ||
| 167 | cd \$HOME/$REPO | ||
| 168 | git checkout -q $BRANCH | ||
| 169 | git reset -q --hard $BRANCH | ||
| 170 | H | ||
| 171 | HOST_SHA=$(hssh 60 <<H | ||
| 172 | cd \$HOME/$REPO && git rev-parse HEAD | ||
| 173 | H | ||
| 174 | ) | ||
| 175 | [ "$HOST_SHA" = "$HEAD_SHA" ] || | ||
| 176 | fail push "the host is at $HOST_SHA, this tree is at $HEAD_SHA" | ||
| 177 | ok push "$MAC:$REPO on $BRANCH at $(git rev-parse --short HEAD), pushed=$PUSHED" | ||
| 178 | |||
| 179 | # ---- leg 2: build — the toolchain lives on the host -------------------- | ||
| 180 | # The unit suite runs HERE and not in the guest for the same reason: it | ||
| 181 | # needs zig, the shadow SDK and the source tree, and the guest has none of | ||
| 182 | # them. `make test` rather than a bare `deps/zig/zig build test` so the | ||
| 183 | # mac-sdk shim is on PATH the way every other target gets it. | ||
| 184 | BOUT=$(hssh 2400 <<H | ||
| 185 | cd \$HOME/$REPO || { echo "RC_BUILD=90"; exit 0; } | ||
| 186 | export PATH=\$HOME/$REPO/deps/mac-sdk/bin:/opt/homebrew/bin:\$PATH | ||
| 187 | make build >/tmp/mac-build.log 2>&1; echo "RC_BUILD=\$?" | ||
| 188 | make check >/tmp/mac-check.log 2>&1; echo "RC_CHECK=\$?" | ||
| 189 | make test >/tmp/mac-test.log 2>&1; echo "RC_TEST=\$?" | ||
| 190 | H | ||
| 191 | ) | ||
| 192 | for _step in BUILD CHECK TEST; do | ||
| 193 | _rc=$(printf '%s\n' "$BOUT" | sed -n "s/^RC_$_step=//p") | ||
| 194 | [ "$_rc" = "0" ] && continue | ||
| 195 | _log=$(printf '%s' "$_step" | tr 'A-Z' 'a-z') | ||
| 196 | # The verdict first and the reading under it, test/vm.sh's shape: a | ||
| 197 | # reader scrolling back finds the sentence that says what broke, not | ||
| 198 | # the tail of somebody else's stack trace. | ||
| 199 | echo "mac FAIL: build: make $_log on $MAC exited ${_rc:-<no answer>}" | ||
| 200 | hssh 60 <<H | sed 's/^/ /' || true | ||
| 201 | tail -30 /tmp/mac-$_log.log 2>/dev/null || true | ||
| 202 | H | ||
| 203 | exit 1 | ||
| 204 | done | ||
| 205 | ok build "make build, check and test on $MAC" | ||
| 206 | |||
| 207 | # ---- leg 3: vm — a pristine guest, cloned and booted ------------------- | ||
| 208 | VM_T0=$(now_ms) | ||
| 209 | hssh 600 >/dev/null <<H || fail vm "could not clone and boot $VM_RUN on $MAC" | ||
| 210 | export PATH=/opt/homebrew/bin:\$PATH | ||
| 211 | tart list 2>/dev/null | awk '{print \$2}' | grep -qx $VM_BASE || { | ||
| 212 | echo "mac FAIL: vm: $MAC has no VM called $VM_BASE." >&2 | ||
| 213 | echo " Make it once by hand; the recipe is this script's header." >&2 | ||
| 214 | exit 1; } | ||
| 215 | tart stop $VM_RUN >/dev/null 2>&1 || true | ||
| 216 | tart delete $VM_RUN >/dev/null 2>&1 || true | ||
| 217 | tart clone $VM_BASE $VM_RUN || exit 1 | ||
| 218 | nohup tart run --no-graphics $VM_RUN >/tmp/$VM_RUN.log 2>&1 & | ||
| 219 | sleep 1 | ||
| 220 | exit 0 | ||
| 221 | H | ||
| 222 | # Polled on the HOST rather than one ssh per tick: a tick costs a whole | ||
| 223 | # connection setup from here and the address usually arrives in seconds. | ||
| 224 | GIP=$(hssh 240 <<H | ||
| 225 | export PATH=/opt/homebrew/bin:\$PATH | ||
| 226 | _i=0 | ||
| 227 | while [ \$_i -lt 60 ]; do | ||
| 228 | _ip=\$(tart ip $VM_RUN 2>/dev/null) && [ -n "\$_ip" ] && { echo "\$_ip"; exit 0; } | ||
| 229 | sleep 2; _i=\$((_i + 1)) | ||
| 230 | done | ||
| 231 | exit 1 | ||
| 232 | H | ||
| 233 | ) || fail vm "$VM_RUN took no address within 120 s (host log: /tmp/$VM_RUN.log)" | ||
| 234 | |||
| 235 | _i=0 | ||
| 236 | while :; do | ||
| 237 | # An `if` and not `gssh ... && break`: under `set -e` an AND-OR list | ||
| 238 | # that ends in a failing test is a failing command, so the first tick | ||
| 239 | # against a guest still booting would end the run. | ||
| 240 | if gssh 25 >/dev/null 2>&1 <<'G' | ||
| 241 | true | ||
| 242 | G | ||
| 243 | then break; fi | ||
| 244 | _i=$((_i + 1)) | ||
| 245 | [ "$_i" -lt 40 ] || fail vm "$VM_RUN at $GIP never answered ssh within 120 s" | ||
| 246 | sleep 3 | ||
| 247 | done | ||
| 248 | VM_MS=$(( $(now_ms) - VM_T0 )) | ||
| 249 | ok vm "$VM_RUN cloned, booted and answering at $GIP in $((VM_MS / 1000))s" | ||
| 250 | |||
| 251 | # ---- leg 4: install — the host's build, onto a bare Mac ---------------- | ||
| 252 | hssh 300 >/dev/null <<H || fail install "could not copy the build into $VM_RUN" | ||
| 253 | set -e | ||
| 254 | cd \$HOME/$REPO | ||
| 255 | ssh -n $GUEST_SSH $GUSER@$GIP "mkdir -p ~/mux-e2e" | ||
| 256 | scp -q $GUEST_SSH zig-out/bin/mux zig-out/bin/ptyclient test/os_oracle.sh $GUSER@$GIP:mux-e2e/ | ||
| 257 | H | ||
| 258 | WANT_VER=$(hssh 60 <<H | ||
| 259 | cd \$HOME/$REPO && ./zig-out/bin/mux --version | ||
| 260 | H | ||
| 261 | ) | ||
| 262 | GOT_VER=$(gssh 60 <<'G' | ||
| 263 | ./mux --version | ||
| 264 | G | ||
| 265 | ) | ||
| 266 | [ -n "$WANT_VER" ] && [ "$WANT_VER" = "$GOT_VER" ] || | ||
| 267 | fail install "the guest answers '$GOT_VER', the host built '$WANT_VER'" | ||
| 268 | BMODE=$(gssh 60 <<'G' | ||
| 269 | . ./os_oracle.sh | ||
| 270 | file_mode ./mux | ||
| 271 | G | ||
| 272 | ) | ||
| 273 | [ "$BMODE" = "755" ] || | ||
| 274 | fail install "the copied binary is mode $BMODE, want 755" | ||
| 275 | ok install "a bare macOS runs the host's build and answers '$GOT_VER'" | ||
| 276 | |||
| 277 | # ---- leg 5: daemon — no XDG_RUNTIME_DIR, so /tmp/mux-<uid> ------------ | ||
| 278 | # The pristine claim, asserted rather than assumed: this is what the | ||
| 279 | # per-run clone buys, and a clone that silently carried a previous run's | ||
| 280 | # daemon would make every leg below it a check on somebody else's state. | ||
| 281 | PRE=$(gssh 60 <<'G' | ||
| 282 | echo "uid=$(id -u)" | ||
| 283 | echo "muxes=$(pgrep -x mux 2>/dev/null | wc -l | tr -d ' ')" | ||
| 284 | [ -e /tmp/mux-$(id -u) ] && echo "rt=present" || echo "rt=absent" | ||
| 285 | G | ||
| 286 | ) | ||
| 287 | GUID=$(printf '%s\n' "$PRE" | sed -n 's/^uid=//p') | ||
| 288 | printf '%s\n' "$PRE" | grep -qx 'muxes=0' || | ||
| 289 | fail daemon "the freshly cloned guest already runs a mux: $PRE" | ||
| 290 | printf '%s\n' "$PRE" | grep -qx 'rt=absent' || | ||
| 291 | fail daemon "the freshly cloned guest already has /tmp/mux-$GUID: $PRE" | ||
| 292 | [ -n "$GUID" ] || fail daemon "the guest would not say its uid" | ||
| 293 | |||
| 294 | # MUX_SHELL_INTEGRATION=1 because the session leg below reads a real exit | ||
| 295 | # code, and marks are opt-in (README). It also puts the daemon's shellint | ||
| 296 | # shim directory inside the runtime directory this leg is about. | ||
| 297 | gssh 120 >/dev/null <<'G' || fail daemon "mux d start -d refused on the guest" | ||
| 298 | MUX_SHELL_INTEGRATION=1 ./mux d start -d | ||
| 299 | G | ||
| 300 | DPID=$(gssh 60 <<'G' | ||
| 301 | pgrep -x mux | head -1 | ||
| 302 | G | ||
| 303 | ) | ||
| 304 | [ -n "$DPID" ] || fail daemon "no mux process on the guest after mux d start -d" | ||
| 305 | |||
| 306 | SOCK="/tmp/mux-$GUID/muxd.sock" | ||
| 307 | gssh 60 >/dev/null <<G || fail daemon "daemon $DPID does not hold $SOCK" | ||
| 308 | . ./os_oracle.sh | ||
| 309 | pid_holds_unix_sock $DPID $SOCK | ||
| 310 | G | ||
| 311 | # canonicalised WITHOUT the oracle's real_path: that spelling is greadlink, | ||
| 312 | # and a pristine macOS has no GNU coreutils on it — installing them to | ||
| 313 | # answer one question would make the guest something other than the bare | ||
| 314 | # Mac this gate is about. `cd -P` resolves a symlinked directory the same | ||
| 315 | # way, and lsof (which is what pid_exe reads) reports the same canonical | ||
| 316 | # form for the last component. | ||
| 317 | DEXE=$(gssh 60 <<'G' | ||
| 318 | . ./os_oracle.sh | ||
| 319 | echo "is=$(pid_exe $(pgrep -x mux | head -1))" | ||
| 320 | echo "want=$(cd -P -- ./ && pwd -P)/mux" | ||
| 321 | G | ||
| 322 | ) | ||
| 323 | _is=$(printf '%s\n' "$DEXE" | sed -n 's/^is=//p') | ||
| 324 | _want=$(printf '%s\n' "$DEXE" | sed -n 's/^want=//p') | ||
| 325 | [ -n "$_want" ] && [ "$_is" = "$_want" ] || | ||
| 326 | fail daemon "daemon $DPID execs '$_is', not the installed '$_want'" | ||
| 327 | RTMODE=$(gssh 60 <<G | ||
| 328 | . ./os_oracle.sh | ||
| 329 | file_mode /tmp/mux-$GUID | ||
| 330 | G | ||
| 331 | ) | ||
| 332 | [ "$RTMODE" = "700" ] || | ||
| 333 | fail daemon "/tmp/mux-$GUID is mode $RTMODE, want 700" | ||
| 334 | ok daemon "daemon $DPID holds $SOCK, a 700 directory the guest had no \$XDG_RUNTIME_DIR for" | ||
| 335 | |||
| 336 | # ---- leg 6: attach — a real pty, a real session ------------------------ | ||
| 337 | # Before the session leg and not after it, though the brief lists them the | ||
| 338 | # other way round: `mux a` attaches at 0x0 and JOINS, so it can only speak | ||
| 339 | # to a session that already exists (README). The pty attach is what makes | ||
| 340 | # one, which makes it the leg that has to run first. | ||
| 341 | ATT=$(gssh 240 <<'G' | ||
| 342 | ./ptyclient --cols 80 --rows 24 --out /tmp/mac-attach.cap --err /tmp/mac-attach.err -- ./mux <<'PC' | ||
| 343 | expect \x1b[?1049h 30000 | ||
| 344 | settle 900 20000 | ||
| 345 | send printf "JOURNEY-%s\\n" ok\n | ||
| 346 | expect JOURNEY-ok 20000 | ||
| 347 | settle 600 15000 | ||
| 348 | send \x1cd | ||
| 349 | waitexit 15000 | ||
| 350 | PC | ||
| 351 | echo "PTYRC=$?" | ||
| 352 | G | ||
| 353 | ) || fail attach "the guest would not run ptyclient" | ||
| 354 | printf '%s\n' "$ATT" | grep -qx 'PTYRC=0' || { | ||
| 355 | echo "mac FAIL: attach: ptyclient did not reach the JOURNEY-ok snapshot: $ATT" | ||
| 356 | gssh 60 <<'G' | sed 's/^/ /' || true | ||
| 357 | cat /tmp/mac-attach.err 2>/dev/null | ||
| 358 | G | ||
| 359 | exit 1; } | ||
| 360 | # The detach was a goodbye and not a tear: the session it typed into is | ||
| 361 | # still on the daemon, holding what it painted. | ||
| 362 | gssh 60 >/dev/null <<'G' || fail attach "the session did not survive the detach" | ||
| 363 | ./mux d dump | grep -q JOURNEY-ok | ||
| 364 | G | ||
| 365 | ok attach "a pty client typed into a session and detached, the grid kept it" | ||
| 366 | |||
| 367 | # ---- leg 7: session — one shot through the agent surface --------------- | ||
| 368 | # $$ expands in the session's OWN shell, so the pid it prints is a pid only | ||
| 369 | # the guest's process table can confirm — which is the assert nothing on | ||
| 370 | # this side can fake. | ||
| 371 | AREPLY=$(gssh 90 <<'G' | ||
| 372 | ./mux a run --timeout 20000 'echo mac-$$' | ||
| 373 | G | ||
| 374 | ) || fail session "mux a run did not answer on the guest" | ||
| 375 | case "$AREPLY" in | ||
| 376 | *'"mechanism":"marks"'*) ;; | ||
| 377 | *) fail session "mux a run answered under no mechanism this leg can read: $AREPLY" ;; | ||
| 378 | esac | ||
| 379 | case "$AREPLY" in | ||
| 380 | *'"exit_code":0'*) ;; | ||
| 381 | *) fail session "mux a run reported no clean exit code: $AREPLY" ;; | ||
| 382 | esac | ||
| 383 | SPID=$(printf '%s\n' "$AREPLY" | sed -n 's/.*mac-\([0-9][0-9]*\).*/\1/p') | ||
| 384 | [ -n "$SPID" ] || fail session "the reply carries no shell pid: $AREPLY" | ||
| 385 | gssh 60 >/dev/null <<G || fail session "shell $SPID is not a child of daemon $DPID" | ||
| 386 | . ./os_oracle.sh | ||
| 387 | pid_children $DPID | grep -qx $SPID | ||
| 388 | G | ||
| 389 | ok session "mux a run answered under marks; shell $SPID is a child of daemon $DPID" | ||
| 390 | |||
| 391 | # ---- leg 8: stop — and the runtime socket goes with it ----------------- | ||
| 392 | gssh 60 >/dev/null <<'G' || fail stop "mux d stop was refused on the guest" | ||
| 393 | ./mux d stop | ||
| 394 | G | ||
| 395 | END=$(gssh 60 <<G | ||
| 396 | . ./os_oracle.sh | ||
| 397 | _i=0 | ||
| 398 | while [ \$_i -lt 30 ]; do | ||
| 399 | pid_alive $DPID || break | ||
| 400 | sleep 0.1; _i=\$((_i + 1)) | ||
| 401 | done | ||
| 402 | pid_alive $DPID && echo "alive=yes" || echo "alive=no" | ||
| 403 | [ -e $SOCK ] && echo "sock=present" || echo "sock=gone" | ||
| 404 | G | ||
| 405 | ) | ||
| 406 | printf '%s\n' "$END" | grep -qx 'alive=no' || | ||
| 407 | fail stop "daemon $DPID survives mux d stop by 3 s: $END" | ||
| 408 | printf '%s\n' "$END" | grep -qx 'sock=gone' || | ||
| 409 | fail stop "$SOCK outlives the daemon that bound it: $END" | ||
| 410 | ok stop "daemon $DPID is gone within 3 s and took $SOCK with it" | ||
| 411 | |||
| 412 | # ---- the pin ---------------------------------------------------------- | ||
| 413 | # A literal, test/vm.sh's style: adding a leg means editing this by hand, | ||
| 414 | # so a leg that stops running cannot pass by not being counted. | ||
| 415 | [ "$OK_COUNT" = "8" ] || { | ||
| 416 | echo "mac FAIL: $OK_COUNT legs ran, the pin says 8 —" | ||
| 417 | echo " a leg was added (update the pin) or silently lost" | ||
| 418 | exit 1 | ||
| 419 | } | ||
| 420 | echo "mac OK ($OK_COUNT legs on $VM_RUN, a pristine macOS on $MAC; boot ${VM_MS}ms)" | ||