a73x

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
test: make mac drives a pristine macOS guest on the Mac the way make vm drives the VM

The macOS half of `make vm`. The Mac host builds, because it holds the
toolchain and the shadow SDK; a macOS guest cloned fresh for every run
only RUNS what the host built, which is the stronger claim — a bare Mac
with no developer tooling on it.

Eight legs, each with its own failure line and each claim asserted by a
separate ssh reading the guest through test/os_oracle.sh: push, build,
vm, install, daemon, attach, session, stop. The daemon leg is where the
Darwin runtime fallback is exercised end to end — a fresh ssh login to a
Mac has no XDG_RUNTIME_DIR, so the daemon binds /tmp/mux-<uid>/muxd.sock
and the oracle grades the directory's mode and the fd that holds it.

`mux a` joins and never creates, so the pty attach that makes the session
runs before the agent leg that talks to it, rather than after as the plan
had them.

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

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)"