test/mac.sh
Ref: Size: 15.7 KiB History
#!/bin/sh
# test/mac.sh — the `make mac` gate: real user journeys on a real macOS.
#
# What `make vm` is to the Linux port, this is to the macOS one. The unit
# suite and the e2e suite grade the code; this grades the PRODUCT on the
# operating system it was ported to, spelled exactly as a user spells it.
#
# Two boxes, taken by name, and the split is the point (test/box_lib.sh):
#
# $MAC_BUILDER a Mac with the toolchain and the shadow SDK on it. It
# BUILDS, and the unit suite runs there for the same reason:
# both need zig, the SDK and the source tree.
# $MAC_BOX the macOS box under test, with no developer tooling on it
# at all. It only RUNS what the builder built, which is the
# stronger claim: a bare Mac and not the machine that
# compiled the binary. Normally a pristine tart guest from
# test/provision-mac.sh, which this gate does not create,
# boot or destroy — a gate that owned a VM's lifecycle could
# not be pointed at a Mac somebody already had.
#
# MAC_BOX is reached DIRECTLY from here: provision-mac.sh bridges the guest
# onto the LAN, so there is no ssh-through-ssh string in this file any more.
# It is a box the gate MAY scrub, and box_scrub is the whole of what that
# means; the scrub runs at the start and again on the way out, so a failed
# run leaves nothing behind and the next one starts from the same place
# whatever happened. Hermeticity used to come from a per-run clone; it comes
# from the scrub now, and the daemon leg still asserts it rather than
# assuming it.
#
# Every claim about MAC_BOX is asserted by a separate ssh reading it through
# test/os_oracle.sh, never by trusting the daemon's own report over the
# connection under test ("ask the OS about the OS").
set -eu
BOX_GATE=mac
MAC_BOX=${MAC_BOX:-}
# The one Mac that is both is the common case, so the builder follows the
# box under test unless it is named. It is never scrubbed and never has a
# mux installed on it: it builds, and that is all.
MAC_BUILDER=${MAC_BUILDER:-$MAC_BOX}
# Relative to the BUILDER's $HOME, because that is how ssh's scp-like remote
# spelling (`HOST:code/rad/mux`) resolves a path.
REPO="${MAC_BUILDER_REPO:-code/rad/mux}"
BRANCH=mac-journey
# now_ms, and the file_mode/pid_* names the MAC_BOX legs source for
# themselves. The harness spells time and paths through the oracle so this
# script would still answer if it were ever run FROM a Mac.
. "$(dirname "$0")/os_oracle.sh"
. "$(dirname "$0")/box_lib.sh"
# A provisioned guest has a new host key every clone, so the harness's own
# ssh to it records none. This gate never dials MAC_BOX with mux, so nothing
# here needs that key in the real known_hosts.
BOX_EPHEMERAL=$MAC_BOX
OK_COUNT=0
ok() {
OK_COUNT=$((OK_COUNT + 1))
echo "$1 ok${2:+ ($2)}"
}
fail() {
echo "mac FAIL: $1: $2"
exit 1
}
# What every command on MAC_BOX runs under. XDG_STATE_HOME is isolated
# because every mux that attaches records its daemon in it (CLAUDE.md), and
# XDG_RUNTIME_DIR is UNSET on purpose: a fresh ssh login to a Mac has no such
# variable, and the Darwin fallback /tmp/mux-<uid> is exactly what the daemon
# leg is here to exercise end to end.
#
# The builder gets the same prelude, where its only effect is a scratch
# directory the cleanup removes: the build legs cd into the repo themselves,
# and no mux ever runs there.
PRELUDE='
export XDG_STATE_HOME=$HOME/mux-e2e/state
unset XDG_RUNTIME_DIR
mkdir -p $HOME/mux-e2e
cd $HOME/mux-e2e
'
# ---- preflight: every box answers, before anything is written ----------
box_preflight MAC_BOX "$MAC_BOX" aarch64 \
'Provision one with: eval "$(test/provision-mac.sh)"'
box_preflight MAC_BUILDER "$MAC_BUILDER" aarch64 \
'A Mac holding this repo with zig and deps/mac-sdk. Defaults to $MAC_BOX.'
# The start scrub, and the one on the way out. The builder is not scrubbed —
# it is not the gate's machine to reset — so its scratch directory is all
# there is to take back.
box_scrub "$MAC_BOX"
cleanup() {
_rc=$?
box_scrub "$MAC_BOX" >/dev/null 2>&1 || true
box_ssh 60 "$MAC_BUILDER" >/dev/null 2>&1 <<'B' || true
rm -rf "$HOME/mux-e2e"
B
exit "$_rc"
}
trap cleanup EXIT
# ---- leg 1: push — the builder builds THIS commit ----------------------
HEAD_SHA=$(git rev-parse HEAD)
PUSHED=no
HAVE=$(box_ssh 60 "$MAC_BUILDER" <<H || true
cd \$HOME/$REPO 2>/dev/null || exit 0
git rev-parse -q --verify refs/heads/$BRANCH 2>/dev/null || true
H
)
if [ "$HAVE" != "$HEAD_SHA" ]; then
# A push cannot update the branch the remote has checked out, and the
# run before this one left it checked out. Detaching first is what
# makes a re-run work; it changes no commit and no file.
box_ssh 60 "$MAC_BUILDER" >/dev/null <<H || fail push "cannot detach $MAC_BUILDER:$REPO before the push"
set -e
cd \$HOME/$REPO
if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then
git checkout -q --detach
fi
H
# --force because this branch is a scratch mirror of whatever this tree
# is and never shared history: a local amend or rebase leaves the
# builder's ref on a commit the new HEAD does not descend from, and a
# plain push would be rejected as a non-fast-forward for the rest of the
# branch's life.
git push --force "$MAC_BUILDER:$REPO" "HEAD:refs/heads/$BRANCH" >/dev/null 2>&1 ||
fail push "git push $MAC_BUILDER:$REPO HEAD:refs/heads/$BRANCH was refused"
PUSHED=yes
fi
box_ssh 60 "$MAC_BUILDER" >/dev/null <<H || fail push "the builder could not check out $BRANCH"
set -e
cd \$HOME/$REPO
git checkout -q $BRANCH
git reset -q --hard $BRANCH
H
BUILDER_SHA=$(box_ssh 60 "$MAC_BUILDER" <<H
cd \$HOME/$REPO && git rev-parse HEAD
H
)
[ "$BUILDER_SHA" = "$HEAD_SHA" ] ||
fail push "the builder is at $BUILDER_SHA, this tree is at $HEAD_SHA"
ok push "$MAC_BUILDER:$REPO on $BRANCH at $(git rev-parse --short HEAD), pushed=$PUSHED"
# ---- leg 2: build — the toolchain lives on the builder -----------------
# The unit suite runs HERE and not on MAC_BOX for the same reason the build
# does: it needs zig, the shadow SDK and the source tree, and the box under
# test has none of them. `make test` rather than a bare `deps/zig/zig build
# test` so the mac-sdk shim is on PATH the way every other target gets it.
BOUT=$(box_ssh 2400 "$MAC_BUILDER" <<H
cd \$HOME/$REPO || { echo "RC_BUILD=90"; exit 0; }
export PATH=\$HOME/$REPO/deps/mac-sdk/bin:/opt/homebrew/bin:\$PATH
make build >/tmp/mac-build.log 2>&1; echo "RC_BUILD=\$?"
make check >/tmp/mac-check.log 2>&1; echo "RC_CHECK=\$?"
make test >/tmp/mac-test.log 2>&1; echo "RC_TEST=\$?"
H
)
for _step in BUILD CHECK TEST; do
_rc=$(printf '%s\n' "$BOUT" | sed -n "s/^RC_$_step=//p")
[ "$_rc" = "0" ] && continue
_log=$(printf '%s' "$_step" | tr 'A-Z' 'a-z')
# The verdict first and the reading under it, test/vm.sh's shape: a
# reader scrolling back finds the sentence that says what broke, not
# the tail of somebody else's stack trace.
echo "mac FAIL: build: make $_log on $MAC_BUILDER exited ${_rc:-<no answer>}"
box_ssh 60 "$MAC_BUILDER" <<H | sed 's/^/ /' || true
tail -30 /tmp/mac-$_log.log 2>/dev/null || true
H
exit 1
done
ok build "make build, check and test on $MAC_BUILDER"
# ---- leg 3: install — the builder's build, onto a bare Mac -------------
# Streamed through this box rather than copied between the two, because
# neither Mac needs a key of the other's for that and only this one can
# reach both. `cat` carries no mode, so box_stream chmods and the two hashes
# are compared below: the bytes that run on MAC_BOX are the bytes the
# builder produced, asserted rather than assumed.
box_ssh 60 "$MAC_BOX" >/dev/null <<'G' || fail install "could not make the scratch directory on $MAC_BOX"
set -e
mkdir -p "$HOME/mux-e2e"
G
for _f in mux ptyclient; do
box_stream "$MAC_BUILDER" "\$HOME/$REPO/zig-out/bin/$_f" "$MAC_BOX" "\$HOME/mux-e2e/$_f" ||
fail install "could not stream $_f from $MAC_BUILDER to $MAC_BOX"
done
box_scp "$MAC_BOX" "$(dirname "$0")/os_oracle.sh" mux-e2e/os_oracle.sh ||
fail install "could not copy the oracle to $MAC_BOX"
WANT=$(box_ssh 60 "$MAC_BUILDER" <<H
cd \$HOME/$REPO
. ./test/os_oracle.sh
echo "ver=\$(./zig-out/bin/mux --version)"
echo "sha=\$(sha256_of ./zig-out/bin/mux)"
H
)
WANT_VER=$(printf '%s\n' "$WANT" | sed -n 's/^ver=//p')
WANT_SHA=$(printf '%s\n' "$WANT" | sed -n 's/^sha=//p')
GOT=$(box_ssh 60 "$MAC_BOX" <<'G'
. ./os_oracle.sh
echo "ver=$(./mux --version)"
echo "sha=$(sha256_of ./mux)"
echo "mode=$(file_mode ./mux)"
G
)
GOT_VER=$(printf '%s\n' "$GOT" | sed -n 's/^ver=//p')
GOT_SHA=$(printf '%s\n' "$GOT" | sed -n 's/^sha=//p')
GOT_MODE=$(printf '%s\n' "$GOT" | sed -n 's/^mode=//p')
[ -n "$WANT_VER" ] && [ "$WANT_VER" = "$GOT_VER" ] ||
fail install "$MAC_BOX answers '$GOT_VER', $MAC_BUILDER built '$WANT_VER'"
[ -n "$WANT_SHA" ] && [ "$WANT_SHA" = "$GOT_SHA" ] ||
fail install "$MAC_BOX holds a mux hashing $GOT_SHA, the builder's is $WANT_SHA"
[ "$GOT_MODE" = "755" ] ||
fail install "the streamed binary is mode $GOT_MODE, want 755"
ok install "a bare macOS runs the builder's own bytes and answers '$GOT_VER'"
# ---- leg 4: daemon — no XDG_RUNTIME_DIR, so /tmp/mux-<uid> ------------
# The scrubbed claim, asserted rather than assumed: this is what the scrub
# buys, and a box that silently carried a previous run's daemon would make
# every leg below it a check on somebody else's state.
PRE=$(box_ssh 60 "$MAC_BOX" <<'G'
echo "uid=$(id -u)"
echo "muxes=$(pgrep -x mux 2>/dev/null | wc -l | tr -d ' ')"
[ -e /tmp/mux-$(id -u) ] && echo "rt=present" || echo "rt=absent"
G
)
GUID=$(printf '%s\n' "$PRE" | sed -n 's/^uid=//p')
printf '%s\n' "$PRE" | grep -qx 'muxes=0' ||
fail daemon "$MAC_BOX still runs a mux after the scrub: $PRE"
printf '%s\n' "$PRE" | grep -qx 'rt=absent' ||
fail daemon "$MAC_BOX still has /tmp/mux-$GUID after the scrub: $PRE"
[ -n "$GUID" ] || fail daemon "$MAC_BOX would not say its uid"
# MUX_SHELL_INTEGRATION=1 because the session leg below reads a real exit
# code, and marks are opt-in (README). It also puts the daemon's shellint
# shim directory inside the runtime directory this leg is about.
box_ssh 120 "$MAC_BOX" >/dev/null <<'G' || fail daemon "mux d start -d refused on $MAC_BOX"
MUX_SHELL_INTEGRATION=1 ./mux d start -d
G
DINFO=$(box_ssh 60 "$MAC_BOX" <<'G'
echo "n=$(pgrep -x mux | wc -l | tr -d ' ')"
echo "pid=$(pgrep -x mux | head -1)"
pgrep -lx mux 2>/dev/null | sed 's/^/ps=/'
G
)
DN=$(printf '%s\n' "$DINFO" | sed -n 's/^n=//p')
DPID=$(printf '%s\n' "$DINFO" | sed -n 's/^pid=//p')
# The COUNT and not just the first pid, test/vm.sh's shape at its own daemon
# leg: with a second mux alive — a stuck starter, a leaked client — `head -1`
# picks one of them, and the socket, exe and stop assertions below then all
# grade whichever it happened to pick, reporting a coin toss as "daemon N
# does not hold the socket". A wrong count also covers "no daemon at all",
# so there is no separate emptiness check.
[ "$DN" = "1" ] || {
echo "mac FAIL: daemon: $MAC_BOX runs ${DN:-<no answer>} mux processes after mux d start -d, want 1:"
printf '%s\n' "$DINFO" | sed -n 's/^ps=/ /p'
exit 1; }
SOCK="/tmp/mux-$GUID/muxd.sock"
box_ssh 60 "$MAC_BOX" >/dev/null <<G || fail daemon "daemon $DPID does not hold $SOCK"
. ./os_oracle.sh
pid_holds_unix_sock $DPID $SOCK
G
# Both sides canonical, and both through the oracle: pid_exe answers what
# lsof reports, which resolves every symlink on the way, so a home directory
# or a $TMPDIR reached through one would fail a correct exec unless the
# expected path is resolved the same way.
DEXE=$(box_ssh 60 "$MAC_BOX" <<G
. ./os_oracle.sh
echo "is=\$(pid_exe $DPID)"
echo "want=\$(real_path ./mux)"
G
)
_is=$(printf '%s\n' "$DEXE" | sed -n 's/^is=//p')
_want=$(printf '%s\n' "$DEXE" | sed -n 's/^want=//p')
[ -n "$_want" ] && [ "$_is" = "$_want" ] ||
fail daemon "daemon $DPID execs '$_is', not the installed '$_want'"
RTMODE=$(box_ssh 60 "$MAC_BOX" <<G
. ./os_oracle.sh
file_mode /tmp/mux-$GUID
G
)
[ "$RTMODE" = "700" ] ||
fail daemon "/tmp/mux-$GUID is mode $RTMODE, want 700"
ok daemon "daemon $DPID holds $SOCK, a 700 directory $MAC_BOX had no \$XDG_RUNTIME_DIR for"
# ---- leg 5: attach — a real pty, a real session ------------------------
# Before the session leg and not after it, though the brief lists them the
# other way round: `mux a` attaches at 0x0 and JOINS, so it can only speak
# to a session that already exists (README). The pty attach is what makes
# one, which makes it the leg that has to run first.
ATT=$(box_ssh 240 "$MAC_BOX" <<'G'
./ptyclient --cols 80 --rows 24 --out /tmp/mac-attach.cap --err /tmp/mac-attach.err -- ./mux <<'PC'
expect \x1b[?1049h 30000
settle 900 20000
send printf "JOURNEY-%s\\n" ok\n
expect JOURNEY-ok 20000
settle 600 15000
send \x1cd
waitexit 15000
PC
echo "PTYRC=$?"
G
) || fail attach "$MAC_BOX would not run ptyclient"
printf '%s\n' "$ATT" | grep -qx 'PTYRC=0' || {
echo "mac FAIL: attach: ptyclient did not reach the JOURNEY-ok snapshot: $ATT"
box_ssh 60 "$MAC_BOX" <<'G' | sed 's/^/ /' || true
cat /tmp/mac-attach.err 2>/dev/null
G
exit 1; }
# The detach was a goodbye and not a tear: the session it typed into is
# still on the daemon, holding what it painted.
box_ssh 60 "$MAC_BOX" >/dev/null <<'G' || fail attach "the session did not survive the detach"
./mux d dump | grep -q JOURNEY-ok
G
ok attach "a pty client typed into a session and detached, the grid kept it"
# ---- leg 6: session — one shot through the agent surface ---------------
# $$ expands in the session's OWN shell, so the pid it prints is a pid only
# the box's process table can confirm — which is the assert nothing on
# this side can fake.
AREPLY=$(box_ssh 90 "$MAC_BOX" <<'G'
./mux a run --timeout 20000 'echo mac-$$'
G
) || fail session "mux a run did not answer on $MAC_BOX"
case "$AREPLY" in
*'"mechanism":"marks"'*) ;;
*) fail session "mux a run answered under no mechanism this leg can read: $AREPLY" ;;
esac
case "$AREPLY" in
*'"exit_code":0'*) ;;
*) fail session "mux a run reported no clean exit code: $AREPLY" ;;
esac
SPID=$(printf '%s\n' "$AREPLY" | sed -n 's/.*mac-\([0-9][0-9]*\).*/\1/p')
[ -n "$SPID" ] || fail session "the reply carries no shell pid: $AREPLY"
box_ssh 60 "$MAC_BOX" >/dev/null <<G || fail session "shell $SPID is not a child of daemon $DPID"
. ./os_oracle.sh
pid_children $DPID | grep -qx $SPID
G
ok session "mux a run answered under marks; shell $SPID is a child of daemon $DPID"
# ---- leg 7: stop — and the runtime socket goes with it -----------------
box_ssh 60 "$MAC_BOX" >/dev/null <<'G' || fail stop "mux d stop was refused on $MAC_BOX"
./mux d stop
G
END=$(box_ssh 60 "$MAC_BOX" <<G
. ./os_oracle.sh
_i=0
while [ \$_i -lt 30 ]; do
pid_alive $DPID || break
sleep 0.1; _i=\$((_i + 1))
done
pid_alive $DPID && echo "alive=yes" || echo "alive=no"
[ -e $SOCK ] && echo "sock=present" || echo "sock=gone"
G
)
printf '%s\n' "$END" | grep -qx 'alive=no' ||
fail stop "daemon $DPID survives mux d stop by 3 s: $END"
printf '%s\n' "$END" | grep -qx 'sock=gone' ||
fail stop "$SOCK outlives the daemon that bound it: $END"
ok stop "daemon $DPID is gone within 3 s and took $SOCK with it"
# ---- the pin ----------------------------------------------------------
# A literal, test/vm.sh's style: adding a leg means editing this by hand,
# so a leg that stops running cannot pass by not being counted. Seven and
# not eight since 2026-09-04: the boot-a-clone leg left with the VM
# lifecycle, and what replaced it is the preflight line above, which is not
# an `ok`.
[ "$OK_COUNT" = "7" ] || {
echo "mac FAIL: $OK_COUNT legs ran, the pin says 7 —"
echo " a leg was added (update the pin) or silently lost"
exit 1
}
echo "mac OK ($OK_COUNT legs on $MAC_BOX, a bare macOS running what $MAC_BUILDER built)"