2e5ce7ef
test: make mac takes MAC_BOX and MAC_BUILDER by name
a73x 2026-09-04 12:50
Commit message
test/mac.sh
| Old | New | ||
|---|---|---|---|
| @@ -5,67 +5,57 @@ | |||
| 5 | # suite and the e2e suite grade the code; this grades the PRODUCT on the | 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. | 6 | # operating system it was ported to, spelled exactly as a user spells it. |
| 7 | # | 7 | # |
| 8 | # Two machines, and the split is the point. The HOST (a Mac, $MUX_MAC) | 8 | # Two boxes, taken by name, and the split is the point (test/box_lib.sh): |
| 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 | # | 9 | # |
| 14 | # The pristine guest is a human action, never harness code (test/vm.sh's | 10 | # $MAC_BUILDER a Mac with the toolchain and the shadow SDK on it. It |
| 15 | # rule). Tooling is Cirrus Labs `tart`; the base image is | 11 | # BUILDS, and the unit suite runs there for the same reason: |
| 16 | # ghcr.io/cirruslabs/macos-tahoe-base (macOS 26). The base VM is made ONCE, | 12 | # both need zig, the SDK and the source tree. |
| 17 | # by hand, on the host: | 13 | # $MAC_BOX the macOS box under test, with no developer tooling on it |
| 14 | # at all. It only RUNS what the builder built, which is the | ||
| 15 | # stronger claim: a bare Mac and not the machine that | ||
| 16 | # compiled the binary. Normally a pristine tart guest from | ||
| 17 | # test/provision-mac.sh, which this gate does not create, | ||
| 18 | # boot or destroy — a gate that owned a VM's lifecycle could | ||
| 19 | # not be pointed at a Mac somebody already had. | ||
| 18 | # | 20 | # |
| 19 | # brew trust cirruslabs/cli && brew install cirruslabs/cli/tart | 21 | # MAC_BOX is reached DIRECTLY from here: provision-mac.sh bridges the guest |
| 20 | # tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest mux-mac-base | 22 | # onto the LAN, so there is no ssh-through-ssh string in this file any more. |
| 21 | # tart run --no-graphics mux-mac-base & # user admin, password admin | 23 | # It is a box the gate MAY scrub, and box_scrub is the whole of what that |
| 22 | # tart exec mux-mac-base sh -c 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "PUBKEY" >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys' | 24 | # means; the scrub runs at the start and again on the way out, so a failed |
| 23 | # tart stop mux-mac-base | 25 | # run leaves nothing behind and the next one starts from the same place |
| 26 | # whatever happened. Hermeticity used to come from a per-run clone; it comes | ||
| 27 | # from the scrub now, and the daemon leg still asserts it rather than | ||
| 28 | # assuming it. | ||
| 24 | # | 29 | # |
| 25 | # PUBKEY is the host's own ~/.ssh/id_ed25519.pub, so the host can ssh into | 30 | # Every claim about MAC_BOX is asserted by a separate ssh reading it through |
| 26 | # the guest without a password; `tart exec` needs the guest agent the | 31 | # test/os_oracle.sh, never by trusting the daemon's own report over the |
| 27 | # cirruslabs images ship. `mux-mac-base` is never RUN by this script: every | 32 | # connection under test ("ask the OS about the OS"). |
| 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 | 33 | set -eu |
| 37 | 34 | ||
| 38 | MAC="${MUX_MAC:-squirtle}" | 35 | BOX_GATE=mac |
| 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 | 36 | ||
| 50 | # BatchMode so a broken auth fails in seconds instead of prompting a | 37 | MAC_BOX=${MAC_BOX:-} |
| 51 | # harness nobody sits in front of. The guest key is accept-anything because | 38 | # The one Mac that is both is the common case, so the builder follows the |
| 52 | # a freshly cloned VM has a host key nobody has ever seen and takes a new | 39 | # box under test unless it is named. It is never scrubbed and never has a |
| 53 | # one on the next run; the guest is reachable only on the host's own | 40 | # mux installed on it: it builds, and that is all. |
| 54 | # virtual network, and the host hop above it is BatchMode-authenticated. | 41 | MAC_BUILDER=${MAC_BUILDER:-$MAC_BOX} |
| 55 | HOST_SSH="-o BatchMode=yes -o ConnectTimeout=10" | 42 | # Relative to the BUILDER's $HOME, because that is how ssh's scp-like remote |
| 56 | # LogLevel=ERROR because a throwaway known-hosts file makes ssh announce the | 43 | # spelling (`HOST:code/rad/mux`) resolves a path. |
| 57 | # new key on stderr every single time, and that line would be most of what a | 44 | REPO="${MAC_BUILDER_REPO:-code/rad/mux}" |
| 58 | # green run prints. | 45 | BRANCH=mac-journey |
| 59 | GUEST_SSH="-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=10 -i $GKEY" | ||
| 60 | 46 | ||
| 61 | # now_ms, and the file_mode/pid_* names the guest legs source for | 47 | # now_ms, and the file_mode/pid_* names the MAC_BOX legs source for |
| 62 | # themselves. The harness spells time and paths through the oracle so this | 48 | # 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. | 49 | # script would still answer if it were ever run FROM a Mac. |
| 64 | . "$(dirname "$0")/os_oracle.sh" | 50 | . "$(dirname "$0")/os_oracle.sh" |
| 51 | . "$(dirname "$0")/box_lib.sh" | ||
| 52 | |||
| 53 | # A provisioned guest has a new host key every clone, so the harness's own | ||
| 54 | # ssh to it records none. This gate never dials MAC_BOX with mux, so nothing | ||
| 55 | # here needs that key in the real known_hosts. | ||
| 56 | BOX_EPHEMERAL=$MAC_BOX | ||
| 65 | 57 | ||
| 66 | GIP="" # the guest's address, set by the vm leg | ||
| 67 | OK_COUNT=0 | 58 | OK_COUNT=0 |
| 68 | PUSHED=no | ||
| 69 | 59 | ||
| 70 | ok() { | 60 | ok() { |
| 71 | OK_COUNT=$((OK_COUNT + 1)) | 61 | OK_COUNT=$((OK_COUNT + 1)) |
| @@ -77,67 +67,46 @@ fail() { | |||
| 77 | exit 1 | 67 | exit 1 |
| 78 | } | 68 | } |
| 79 | 69 | ||
| 80 | # hssh SECONDS — run the script on stdin on the HOST, under /bin/sh. | 70 | # What every command on MAC_BOX runs under. XDG_STATE_HOME is isolated |
| 81 | # | 71 | # because every mux that attaches records its daemon in it (CLAUDE.md), and |
| 82 | # /bin/sh and not the host's login shell (zsh): `$VAR` holding several | 72 | # XDG_RUNTIME_DIR is UNSET on purpose: a fresh ssh login to a Mac has no such |
| 83 | # words does not word-split in zsh, so an options string pasted into a | 73 | # variable, and the Darwin fallback /tmp/mux-<uid> is exactly what the daemon |
| 84 | # command line there arrives as ONE argument and ssh answers "keyword | 74 | # leg is here to exercise end to end. |
| 85 | # batchmode extra arguments at end of line". | ||
| 86 | # | 75 | # |
| 87 | # The rule for a script handed to this: any ssh INSIDE it takes -n. The | 76 | # The builder gets the same prelude, where its only effect is a scratch |
| 88 | # host's sh is reading its own script off stdin, and an ssh that does not | 77 | # directory the cleanup removes: the build legs cd into the repo themselves, |
| 89 | # close its stdin swallows the rest of that script — silently, with a 0 | 78 | # and no mux ever runs there. |
| 90 | # exit, so the lines after it simply never run. | 79 | PRELUDE=' |
| 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 | 80 | export XDG_STATE_HOME=$HOME/mux-e2e/state |
| 104 | unset XDG_RUNTIME_DIR | 81 | unset XDG_RUNTIME_DIR |
| 105 | mkdir -p $HOME/mux-e2e | 82 | mkdir -p $HOME/mux-e2e |
| 106 | cd $HOME/mux-e2e | 83 | cd $HOME/mux-e2e |
| 107 | ' | 84 | ' |
| 108 | 85 | ||
| 109 | # gssh SECONDS — run the script on stdin in the GUEST, through the host. | 86 | # ---- preflight: every box answers, before anything is written ---------- |
| 110 | # | 87 | box_preflight MAC_BOX "$MAC_BOX" aarch64 \ |
| 111 | # The quoting rule is that there is NO quoting. The script rides this | 88 | 'Provision one with: eval "$(test/provision-mac.sh)"' |
| 112 | # shell's stdin into the host's ssh, which forwards it to the guest's | 89 | box_preflight MAC_BUILDER "$MAC_BUILDER" aarch64 \ |
| 113 | # `sh -s`; no shell between here and there parses it as a command line, so | 90 | 'A Mac holding this repo with zig and deps/mac-sdk. Defaults to $MAC_BOX.' |
| 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 | 91 | ||
| 125 | # The clone is stopped on every exit, including a failing one. It is left | 92 | # The start scrub, and the one on the way out. The builder is not scrubbed — |
| 126 | # in place rather than deleted so a failure can be logged into, and the | 93 | # it is not the gate's machine to reset — so its scratch directory is all |
| 127 | # next run replaces it. | 94 | # there is to take back. |
| 95 | box_scrub "$MAC_BOX" | ||
| 128 | cleanup() { | 96 | cleanup() { |
| 129 | _rc=$? | 97 | _rc=$? |
| 130 | hssh 180 >/dev/null 2>&1 <<H || true | 98 | box_scrub "$MAC_BOX" >/dev/null 2>&1 || true |
| 131 | export PATH=/opt/homebrew/bin:\$PATH | 99 | box_ssh 60 "$MAC_BUILDER" >/dev/null 2>&1 <<'B' || true |
| 132 | tart stop $VM_RUN 2>/dev/null || true | 100 | rm -rf "$HOME/mux-e2e" |
| 133 | H | 101 | B |
| 134 | exit "$_rc" | 102 | exit "$_rc" |
| 135 | } | 103 | } |
| 136 | trap cleanup EXIT | 104 | trap cleanup EXIT |
| 137 | 105 | ||
| 138 | # ---- leg 1: push — the host builds THIS commit ------------------------ | 106 | # ---- leg 1: push — the builder builds THIS commit ---------------------- |
| 139 | HEAD_SHA=$(git rev-parse HEAD) | 107 | HEAD_SHA=$(git rev-parse HEAD) |
| 140 | HAVE=$(hssh 60 <<H || true | 108 | PUSHED=no |
| 109 | HAVE=$(box_ssh 60 "$MAC_BUILDER" <<H || true | ||
| 141 | cd \$HOME/$REPO 2>/dev/null || exit 0 | 110 | cd \$HOME/$REPO 2>/dev/null || exit 0 |
| 142 | git rev-parse -q --verify refs/heads/$BRANCH 2>/dev/null || true | 111 | git rev-parse -q --verify refs/heads/$BRANCH 2>/dev/null || true |
| 143 | H | 112 | H |
| @@ -146,7 +115,7 @@ if [ "$HAVE" != "$HEAD_SHA" ]; then | |||
| 146 | # A push cannot update the branch the remote has checked out, and the | 115 | # 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 | 116 | # 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. | 117 | # 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" | 118 | box_ssh 60 "$MAC_BUILDER" >/dev/null <<H || fail push "cannot detach $MAC_BUILDER:$REPO before the push" |
| 150 | set -e | 119 | set -e |
| 151 | cd \$HOME/$REPO | 120 | cd \$HOME/$REPO |
| 152 | if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then | 121 | if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then |
| @@ -154,34 +123,34 @@ if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then | |||
| 154 | fi | 123 | fi |
| 155 | H | 124 | H |
| 156 | # --force because this branch is a scratch mirror of whatever this tree | 125 | # --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 | 126 | # is and never shared history: a local amend or rebase leaves the |
| 158 | # ref on a commit the new HEAD does not descend from, and a plain push | 127 | # builder's ref on a commit the new HEAD does not descend from, and a |
| 159 | # would be rejected as a non-fast-forward for the rest of the branch's | 128 | # plain push would be rejected as a non-fast-forward for the rest of the |
| 160 | # life. | 129 | # branch's life. |
| 161 | git push --force "$MAC:$REPO" "HEAD:refs/heads/$BRANCH" >/dev/null 2>&1 || | 130 | git push --force "$MAC_BUILDER:$REPO" "HEAD:refs/heads/$BRANCH" >/dev/null 2>&1 || |
| 162 | fail push "git push $MAC:$REPO HEAD:refs/heads/$BRANCH was refused" | 131 | fail push "git push $MAC_BUILDER:$REPO HEAD:refs/heads/$BRANCH was refused" |
| 163 | PUSHED=yes | 132 | PUSHED=yes |
| 164 | fi | 133 | fi |
| 165 | hssh 60 >/dev/null <<H || fail push "the host could not check out $BRANCH" | 134 | box_ssh 60 "$MAC_BUILDER" >/dev/null <<H || fail push "the builder could not check out $BRANCH" |
| 166 | set -e | 135 | set -e |
| 167 | cd \$HOME/$REPO | 136 | cd \$HOME/$REPO |
| 168 | git checkout -q $BRANCH | 137 | git checkout -q $BRANCH |
| 169 | git reset -q --hard $BRANCH | 138 | git reset -q --hard $BRANCH |
| 170 | H | 139 | H |
| 171 | HOST_SHA=$(hssh 60 <<H | 140 | BUILDER_SHA=$(box_ssh 60 "$MAC_BUILDER" <<H |
| 172 | cd \$HOME/$REPO && git rev-parse HEAD | 141 | cd \$HOME/$REPO && git rev-parse HEAD |
| 173 | H | 142 | H |
| 174 | ) | 143 | ) |
| 175 | [ "$HOST_SHA" = "$HEAD_SHA" ] || | 144 | [ "$BUILDER_SHA" = "$HEAD_SHA" ] || |
| 176 | fail push "the host is at $HOST_SHA, this tree is at $HEAD_SHA" | 145 | fail push "the builder is at $BUILDER_SHA, this tree is at $HEAD_SHA" |
| 177 | ok push "$MAC:$REPO on $BRANCH at $(git rev-parse --short HEAD), pushed=$PUSHED" | 146 | ok push "$MAC_BUILDER:$REPO on $BRANCH at $(git rev-parse --short HEAD), pushed=$PUSHED" |
| 178 | 147 | ||
| 179 | # ---- leg 2: build — the toolchain lives on the host -------------------- | 148 | # ---- leg 2: build — the toolchain lives on the builder ----------------- |
| 180 | # The unit suite runs HERE and not in the guest for the same reason: it | 149 | # The unit suite runs HERE and not on MAC_BOX for the same reason the build |
| 181 | # needs zig, the shadow SDK and the source tree, and the guest has none of | 150 | # does: it needs zig, the shadow SDK and the source tree, and the box under |
| 182 | # them. `make test` rather than a bare `deps/zig/zig build test` so the | 151 | # test has none of them. `make test` rather than a bare `deps/zig/zig build |
| 183 | # mac-sdk shim is on PATH the way every other target gets it. | 152 | # test` so the mac-sdk shim is on PATH the way every other target gets it. |
| 184 | BOUT=$(hssh 2400 <<H | 153 | BOUT=$(box_ssh 2400 "$MAC_BUILDER" <<H |
| 185 | cd \$HOME/$REPO || { echo "RC_BUILD=90"; exit 0; } | 154 | cd \$HOME/$REPO || { echo "RC_BUILD=90"; exit 0; } |
| 186 | export PATH=\$HOME/$REPO/deps/mac-sdk/bin:/opt/homebrew/bin:\$PATH | 155 | export PATH=\$HOME/$REPO/deps/mac-sdk/bin:/opt/homebrew/bin:\$PATH |
| 187 | make build >/tmp/mac-build.log 2>&1; echo "RC_BUILD=\$?" | 156 | make build >/tmp/mac-build.log 2>&1; echo "RC_BUILD=\$?" |
| @@ -196,89 +165,62 @@ for _step in BUILD CHECK TEST; do | |||
| 196 | # The verdict first and the reading under it, test/vm.sh's shape: a | 165 | # 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 | 166 | # reader scrolling back finds the sentence that says what broke, not |
| 198 | # the tail of somebody else's stack trace. | 167 | # the tail of somebody else's stack trace. |
| 199 | echo "mac FAIL: build: make $_log on $MAC exited ${_rc:-<no answer>}" | 168 | echo "mac FAIL: build: make $_log on $MAC_BUILDER exited ${_rc:-<no answer>}" |
| 200 | hssh 60 <<H | sed 's/^/ /' || true | 169 | box_ssh 60 "$MAC_BUILDER" <<H | sed 's/^/ /' || true |
| 201 | tail -30 /tmp/mac-$_log.log 2>/dev/null || true | 170 | tail -30 /tmp/mac-$_log.log 2>/dev/null || true |
| 202 | H | 171 | H |
| 203 | exit 1 | 172 | exit 1 |
| 204 | done | 173 | done |
| 205 | ok build "make build, check and test on $MAC" | 174 | ok build "make build, check and test on $MAC_BUILDER" |
| 206 | 175 | ||
| 207 | # ---- leg 3: vm — a pristine guest, cloned and booted ------------------- | 176 | # ---- leg 3: install — the builder's build, onto a bare Mac ------------- |
| 208 | VM_T0=$(now_ms) | 177 | # Streamed through this box rather than copied between the two, because |
| 209 | hssh 600 >/dev/null <<H || fail vm "could not clone and boot $VM_RUN on $MAC" | 178 | # neither Mac needs a key of the other's for that and only this one can |
| 210 | export PATH=/opt/homebrew/bin:\$PATH | 179 | # reach both. `cat` carries no mode, so box_stream chmods and the two hashes |
| 211 | tart list 2>/dev/null | awk '{print \$2}' | grep -qx $VM_BASE || { | 180 | # are compared below: the bytes that run on MAC_BOX are the bytes the |
| 212 | echo "mac FAIL: vm: $MAC has no VM called $VM_BASE." >&2 | 181 | # builder produced, asserted rather than assumed. |
| 213 | echo " Make it once by hand; the recipe is this script's header." >&2 | 182 | box_ssh 60 "$MAC_BOX" >/dev/null <<'G' || fail install "could not make the scratch directory on $MAC_BOX" |
| 214 | exit 1; } | 183 | set -e |
| 215 | tart stop $VM_RUN >/dev/null 2>&1 || true | 184 | mkdir -p "$HOME/mux-e2e" |
| 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 | 185 | G |
| 243 | then break; fi | 186 | for _f in mux ptyclient; do |
| 244 | _i=$((_i + 1)) | 187 | box_stream "$MAC_BUILDER" "\$HOME/$REPO/zig-out/bin/$_f" "$MAC_BOX" "\$HOME/mux-e2e/$_f" || |
| 245 | [ "$_i" -lt 40 ] || fail vm "$VM_RUN at $GIP never answered ssh within 120 s" | 188 | fail install "could not stream $_f from $MAC_BUILDER to $MAC_BOX" |
| 246 | sleep 3 | ||
| 247 | done | 189 | done |
| 248 | VM_MS=$(( $(now_ms) - VM_T0 )) | 190 | box_scp "$MAC_BOX" "$(dirname "$0")/os_oracle.sh" mux-e2e/os_oracle.sh || |
| 249 | ok vm "$VM_RUN cloned, booted and answering at $GIP in $((VM_MS / 1000))s" | 191 | fail install "could not copy the oracle to $MAC_BOX" |
| 250 | 192 | WANT=$(box_ssh 60 "$MAC_BUILDER" <<H | |
| 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 | 193 | cd \$HOME/$REPO |
| 255 | ssh -n $GUEST_SSH $GUSER@$GIP "mkdir -p ~/mux-e2e" | 194 | . ./test/os_oracle.sh |
| 256 | scp -q $GUEST_SSH zig-out/bin/mux zig-out/bin/ptyclient test/os_oracle.sh $GUSER@$GIP:mux-e2e/ | 195 | echo "ver=\$(./zig-out/bin/mux --version)" |
| 257 | H | 196 | echo "sha=\$(sha256_of ./zig-out/bin/mux)" |
| 258 | WANT_VER=$(hssh 60 <<H | ||
| 259 | cd \$HOME/$REPO && ./zig-out/bin/mux --version | ||
| 260 | H | 197 | H |
| 261 | ) | 198 | ) |
| 262 | GOT_VER=$(gssh 60 <<'G' | 199 | WANT_VER=$(printf '%s\n' "$WANT" | sed -n 's/^ver=//p') |
| 263 | ./mux --version | 200 | WANT_SHA=$(printf '%s\n' "$WANT" | sed -n 's/^sha=//p') |
| 264 | G | 201 | GOT=$(box_ssh 60 "$MAC_BOX" <<'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 | 202 | . ./os_oracle.sh |
| 270 | file_mode ./mux | 203 | echo "ver=$(./mux --version)" |
| 204 | echo "sha=$(sha256_of ./mux)" | ||
| 205 | echo "mode=$(file_mode ./mux)" | ||
| 271 | G | 206 | G |
| 272 | ) | 207 | ) |
| 273 | [ "$BMODE" = "755" ] || | 208 | GOT_VER=$(printf '%s\n' "$GOT" | sed -n 's/^ver=//p') |
| 274 | fail install "the copied binary is mode $BMODE, want 755" | 209 | GOT_SHA=$(printf '%s\n' "$GOT" | sed -n 's/^sha=//p') |
| 275 | ok install "a bare macOS runs the host's build and answers '$GOT_VER'" | 210 | GOT_MODE=$(printf '%s\n' "$GOT" | sed -n 's/^mode=//p') |
| 211 | [ -n "$WANT_VER" ] && [ "$WANT_VER" = "$GOT_VER" ] || | ||
| 212 | fail install "$MAC_BOX answers '$GOT_VER', $MAC_BUILDER built '$WANT_VER'" | ||
| 213 | [ -n "$WANT_SHA" ] && [ "$WANT_SHA" = "$GOT_SHA" ] || | ||
| 214 | fail install "$MAC_BOX holds a mux hashing $GOT_SHA, the builder's is $WANT_SHA" | ||
| 215 | [ "$GOT_MODE" = "755" ] || | ||
| 216 | fail install "the streamed binary is mode $GOT_MODE, want 755" | ||
| 217 | ok install "a bare macOS runs the builder's own bytes and answers '$GOT_VER'" | ||
| 276 | 218 | ||
| 277 | # ---- leg 5: daemon — no XDG_RUNTIME_DIR, so /tmp/mux-<uid> ------------ | 219 | # ---- leg 4: daemon — no XDG_RUNTIME_DIR, so /tmp/mux-<uid> ------------ |
| 278 | # The pristine claim, asserted rather than assumed: this is what the | 220 | # The scrubbed claim, asserted rather than assumed: this is what the scrub |
| 279 | # per-run clone buys, and a clone that silently carried a previous run's | 221 | # buys, and a box that silently carried a previous run's daemon would make |
| 280 | # daemon would make every leg below it a check on somebody else's state. | 222 | # every leg below it a check on somebody else's state. |
| 281 | PRE=$(gssh 60 <<'G' | 223 | PRE=$(box_ssh 60 "$MAC_BOX" <<'G' |
| 282 | echo "uid=$(id -u)" | 224 | echo "uid=$(id -u)" |
| 283 | echo "muxes=$(pgrep -x mux 2>/dev/null | wc -l | tr -d ' ')" | 225 | echo "muxes=$(pgrep -x mux 2>/dev/null | wc -l | tr -d ' ')" |
| 284 | [ -e /tmp/mux-$(id -u) ] && echo "rt=present" || echo "rt=absent" | 226 | [ -e /tmp/mux-$(id -u) ] && echo "rt=present" || echo "rt=absent" |
| @@ -286,18 +228,18 @@ G | |||
| 286 | ) | 228 | ) |
| 287 | GUID=$(printf '%s\n' "$PRE" | sed -n 's/^uid=//p') | 229 | GUID=$(printf '%s\n' "$PRE" | sed -n 's/^uid=//p') |
| 288 | printf '%s\n' "$PRE" | grep -qx 'muxes=0' || | 230 | printf '%s\n' "$PRE" | grep -qx 'muxes=0' || |
| 289 | fail daemon "the freshly cloned guest already runs a mux: $PRE" | 231 | fail daemon "$MAC_BOX still runs a mux after the scrub: $PRE" |
| 290 | printf '%s\n' "$PRE" | grep -qx 'rt=absent' || | 232 | printf '%s\n' "$PRE" | grep -qx 'rt=absent' || |
| 291 | fail daemon "the freshly cloned guest already has /tmp/mux-$GUID: $PRE" | 233 | fail daemon "$MAC_BOX still has /tmp/mux-$GUID after the scrub: $PRE" |
| 292 | [ -n "$GUID" ] || fail daemon "the guest would not say its uid" | 234 | [ -n "$GUID" ] || fail daemon "$MAC_BOX would not say its uid" |
| 293 | 235 | ||
| 294 | # MUX_SHELL_INTEGRATION=1 because the session leg below reads a real exit | 236 | # 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 | 237 | # 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. | 238 | # 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" | 239 | box_ssh 120 "$MAC_BOX" >/dev/null <<'G' || fail daemon "mux d start -d refused on $MAC_BOX" |
| 298 | MUX_SHELL_INTEGRATION=1 ./mux d start -d | 240 | MUX_SHELL_INTEGRATION=1 ./mux d start -d |
| 299 | G | 241 | G |
| 300 | DINFO=$(gssh 60 <<'G' | 242 | DINFO=$(box_ssh 60 "$MAC_BOX" <<'G' |
| 301 | echo "n=$(pgrep -x mux | wc -l | tr -d ' ')" | 243 | echo "n=$(pgrep -x mux | wc -l | tr -d ' ')" |
| 302 | echo "pid=$(pgrep -x mux | head -1)" | 244 | echo "pid=$(pgrep -x mux | head -1)" |
| 303 | pgrep -lx mux 2>/dev/null | sed 's/^/ps=/' | 245 | pgrep -lx mux 2>/dev/null | sed 's/^/ps=/' |
| @@ -312,12 +254,12 @@ DPID=$(printf '%s\n' "$DINFO" | sed -n 's/^pid=//p') | |||
| 312 | # does not hold the socket". A wrong count also covers "no daemon at all", | 254 | # does not hold the socket". A wrong count also covers "no daemon at all", |
| 313 | # so there is no separate emptiness check. | 255 | # so there is no separate emptiness check. |
| 314 | [ "$DN" = "1" ] || { | 256 | [ "$DN" = "1" ] || { |
| 315 | echo "mac FAIL: daemon: the guest runs ${DN:-<no answer>} mux processes after mux d start -d, want 1:" | 257 | echo "mac FAIL: daemon: $MAC_BOX runs ${DN:-<no answer>} mux processes after mux d start -d, want 1:" |
| 316 | printf '%s\n' "$DINFO" | sed -n 's/^ps=/ /p' | 258 | printf '%s\n' "$DINFO" | sed -n 's/^ps=/ /p' |
| 317 | exit 1; } | 259 | exit 1; } |
| 318 | 260 | ||
| 319 | SOCK="/tmp/mux-$GUID/muxd.sock" | 261 | SOCK="/tmp/mux-$GUID/muxd.sock" |
| 320 | gssh 60 >/dev/null <<G || fail daemon "daemon $DPID does not hold $SOCK" | 262 | box_ssh 60 "$MAC_BOX" >/dev/null <<G || fail daemon "daemon $DPID does not hold $SOCK" |
| 321 | . ./os_oracle.sh | 263 | . ./os_oracle.sh |
| 322 | pid_holds_unix_sock $DPID $SOCK | 264 | pid_holds_unix_sock $DPID $SOCK |
| 323 | G | 265 | G |
| @@ -325,7 +267,7 @@ G | |||
| 325 | # lsof reports, which resolves every symlink on the way, so a home directory | 267 | # lsof reports, which resolves every symlink on the way, so a home directory |
| 326 | # or a $TMPDIR reached through one would fail a correct exec unless the | 268 | # or a $TMPDIR reached through one would fail a correct exec unless the |
| 327 | # expected path is resolved the same way. | 269 | # expected path is resolved the same way. |
| 328 | DEXE=$(gssh 60 <<G | 270 | DEXE=$(box_ssh 60 "$MAC_BOX" <<G |
| 329 | . ./os_oracle.sh | 271 | . ./os_oracle.sh |
| 330 | echo "is=\$(pid_exe $DPID)" | 272 | echo "is=\$(pid_exe $DPID)" |
| 331 | echo "want=\$(real_path ./mux)" | 273 | echo "want=\$(real_path ./mux)" |
| @@ -335,21 +277,21 @@ _is=$(printf '%s\n' "$DEXE" | sed -n 's/^is=//p') | |||
| 335 | _want=$(printf '%s\n' "$DEXE" | sed -n 's/^want=//p') | 277 | _want=$(printf '%s\n' "$DEXE" | sed -n 's/^want=//p') |
| 336 | [ -n "$_want" ] && [ "$_is" = "$_want" ] || | 278 | [ -n "$_want" ] && [ "$_is" = "$_want" ] || |
| 337 | fail daemon "daemon $DPID execs '$_is', not the installed '$_want'" | 279 | fail daemon "daemon $DPID execs '$_is', not the installed '$_want'" |
| 338 | RTMODE=$(gssh 60 <<G | 280 | RTMODE=$(box_ssh 60 "$MAC_BOX" <<G |
| 339 | . ./os_oracle.sh | 281 | . ./os_oracle.sh |
| 340 | file_mode /tmp/mux-$GUID | 282 | file_mode /tmp/mux-$GUID |
| 341 | G | 283 | G |
| 342 | ) | 284 | ) |
| 343 | [ "$RTMODE" = "700" ] || | 285 | [ "$RTMODE" = "700" ] || |
| 344 | fail daemon "/tmp/mux-$GUID is mode $RTMODE, want 700" | 286 | fail daemon "/tmp/mux-$GUID is mode $RTMODE, want 700" |
| 345 | ok daemon "daemon $DPID holds $SOCK, a 700 directory the guest had no \$XDG_RUNTIME_DIR for" | 287 | ok daemon "daemon $DPID holds $SOCK, a 700 directory $MAC_BOX had no \$XDG_RUNTIME_DIR for" |
| 346 | 288 | ||
| 347 | # ---- leg 6: attach — a real pty, a real session ------------------------ | 289 | # ---- leg 5: attach — a real pty, a real session ------------------------ |
| 348 | # Before the session leg and not after it, though the brief lists them the | 290 | # Before the session leg and not after it, though the brief lists them the |
| 349 | # other way round: `mux a` attaches at 0x0 and JOINS, so it can only speak | 291 | # other way round: `mux a` attaches at 0x0 and JOINS, so it can only speak |
| 350 | # to a session that already exists (README). The pty attach is what makes | 292 | # to a session that already exists (README). The pty attach is what makes |
| 351 | # one, which makes it the leg that has to run first. | 293 | # one, which makes it the leg that has to run first. |
| 352 | ATT=$(gssh 240 <<'G' | 294 | ATT=$(box_ssh 240 "$MAC_BOX" <<'G' |
| 353 | ./ptyclient --cols 80 --rows 24 --out /tmp/mac-attach.cap --err /tmp/mac-attach.err -- ./mux <<'PC' | 295 | ./ptyclient --cols 80 --rows 24 --out /tmp/mac-attach.cap --err /tmp/mac-attach.err -- ./mux <<'PC' |
| 354 | expect \x1b[?1049h 30000 | 296 | expect \x1b[?1049h 30000 |
| 355 | settle 900 20000 | 297 | settle 900 20000 |
| @@ -361,28 +303,28 @@ waitexit 15000 | |||
| 361 | PC | 303 | PC |
| 362 | echo "PTYRC=$?" | 304 | echo "PTYRC=$?" |
| 363 | G | 305 | G |
| 364 | ) || fail attach "the guest would not run ptyclient" | 306 | ) || fail attach "$MAC_BOX would not run ptyclient" |
| 365 | printf '%s\n' "$ATT" | grep -qx 'PTYRC=0' || { | 307 | printf '%s\n' "$ATT" | grep -qx 'PTYRC=0' || { |
| 366 | echo "mac FAIL: attach: ptyclient did not reach the JOURNEY-ok snapshot: $ATT" | 308 | echo "mac FAIL: attach: ptyclient did not reach the JOURNEY-ok snapshot: $ATT" |
| 367 | gssh 60 <<'G' | sed 's/^/ /' || true | 309 | box_ssh 60 "$MAC_BOX" <<'G' | sed 's/^/ /' || true |
| 368 | cat /tmp/mac-attach.err 2>/dev/null | 310 | cat /tmp/mac-attach.err 2>/dev/null |
| 369 | G | 311 | G |
| 370 | exit 1; } | 312 | exit 1; } |
| 371 | # The detach was a goodbye and not a tear: the session it typed into is | 313 | # The detach was a goodbye and not a tear: the session it typed into is |
| 372 | # still on the daemon, holding what it painted. | 314 | # still on the daemon, holding what it painted. |
| 373 | gssh 60 >/dev/null <<'G' || fail attach "the session did not survive the detach" | 315 | box_ssh 60 "$MAC_BOX" >/dev/null <<'G' || fail attach "the session did not survive the detach" |
| 374 | ./mux d dump | grep -q JOURNEY-ok | 316 | ./mux d dump | grep -q JOURNEY-ok |
| 375 | G | 317 | G |
| 376 | ok attach "a pty client typed into a session and detached, the grid kept it" | 318 | ok attach "a pty client typed into a session and detached, the grid kept it" |
| 377 | 319 | ||
| 378 | # ---- leg 7: session — one shot through the agent surface --------------- | 320 | # ---- leg 6: session — one shot through the agent surface --------------- |
| 379 | # $$ expands in the session's OWN shell, so the pid it prints is a pid only | 321 | # $$ expands in the session's OWN shell, so the pid it prints is a pid only |
| 380 | # the guest's process table can confirm — which is the assert nothing on | 322 | # the box's process table can confirm — which is the assert nothing on |
| 381 | # this side can fake. | 323 | # this side can fake. |
| 382 | AREPLY=$(gssh 90 <<'G' | 324 | AREPLY=$(box_ssh 90 "$MAC_BOX" <<'G' |
| 383 | ./mux a run --timeout 20000 'echo mac-$$' | 325 | ./mux a run --timeout 20000 'echo mac-$$' |
| 384 | G | 326 | G |
| 385 | ) || fail session "mux a run did not answer on the guest" | 327 | ) || fail session "mux a run did not answer on $MAC_BOX" |
| 386 | case "$AREPLY" in | 328 | case "$AREPLY" in |
| 387 | *'"mechanism":"marks"'*) ;; | 329 | *'"mechanism":"marks"'*) ;; |
| 388 | *) fail session "mux a run answered under no mechanism this leg can read: $AREPLY" ;; | 330 | *) fail session "mux a run answered under no mechanism this leg can read: $AREPLY" ;; |
| @@ -393,17 +335,17 @@ case "$AREPLY" in | |||
| 393 | esac | 335 | esac |
| 394 | SPID=$(printf '%s\n' "$AREPLY" | sed -n 's/.*mac-\([0-9][0-9]*\).*/\1/p') | 336 | SPID=$(printf '%s\n' "$AREPLY" | sed -n 's/.*mac-\([0-9][0-9]*\).*/\1/p') |
| 395 | [ -n "$SPID" ] || fail session "the reply carries no shell pid: $AREPLY" | 337 | [ -n "$SPID" ] || fail session "the reply carries no shell pid: $AREPLY" |
| 396 | gssh 60 >/dev/null <<G || fail session "shell $SPID is not a child of daemon $DPID" | 338 | box_ssh 60 "$MAC_BOX" >/dev/null <<G || fail session "shell $SPID is not a child of daemon $DPID" |
| 397 | . ./os_oracle.sh | 339 | . ./os_oracle.sh |
| 398 | pid_children $DPID | grep -qx $SPID | 340 | pid_children $DPID | grep -qx $SPID |
| 399 | G | 341 | G |
| 400 | ok session "mux a run answered under marks; shell $SPID is a child of daemon $DPID" | 342 | ok session "mux a run answered under marks; shell $SPID is a child of daemon $DPID" |
| 401 | 343 | ||
| 402 | # ---- leg 8: stop — and the runtime socket goes with it ----------------- | 344 | # ---- leg 7: stop — and the runtime socket goes with it ----------------- |
| 403 | gssh 60 >/dev/null <<'G' || fail stop "mux d stop was refused on the guest" | 345 | box_ssh 60 "$MAC_BOX" >/dev/null <<'G' || fail stop "mux d stop was refused on $MAC_BOX" |
| 404 | ./mux d stop | 346 | ./mux d stop |
| 405 | G | 347 | G |
| 406 | END=$(gssh 60 <<G | 348 | END=$(box_ssh 60 "$MAC_BOX" <<G |
| 407 | . ./os_oracle.sh | 349 | . ./os_oracle.sh |
| 408 | _i=0 | 350 | _i=0 |
| 409 | while [ \$_i -lt 30 ]; do | 351 | while [ \$_i -lt 30 ]; do |
| @@ -422,10 +364,13 @@ ok stop "daemon $DPID is gone within 3 s and took $SOCK with it" | |||
| 422 | 364 | ||
| 423 | # ---- the pin ---------------------------------------------------------- | 365 | # ---- the pin ---------------------------------------------------------- |
| 424 | # A literal, test/vm.sh's style: adding a leg means editing this by hand, | 366 | # A literal, test/vm.sh's style: adding a leg means editing this by hand, |
| 425 | # so a leg that stops running cannot pass by not being counted. | 367 | # so a leg that stops running cannot pass by not being counted. Seven and |
| 426 | [ "$OK_COUNT" = "8" ] || { | 368 | # not eight since 2026-09-04: the boot-a-clone leg left with the VM |
| 427 | echo "mac FAIL: $OK_COUNT legs ran, the pin says 8 —" | 369 | # lifecycle, and what replaced it is the preflight line above, which is not |
| 370 | # an `ok`. | ||
| 371 | [ "$OK_COUNT" = "7" ] || { | ||
| 372 | echo "mac FAIL: $OK_COUNT legs ran, the pin says 7 —" | ||
| 428 | echo " a leg was added (update the pin) or silently lost" | 373 | echo " a leg was added (update the pin) or silently lost" |
| 429 | exit 1 | 374 | exit 1 |
| 430 | } | 375 | } |
| 431 | echo "mac OK ($OK_COUNT legs on $VM_RUN, a pristine macOS on $MAC; boot ${VM_MS}ms)" | 376 | echo "mac OK ($OK_COUNT legs on $MAC_BOX, a bare macOS running what $MAC_BUILDER built)" |