a73x

8f8ed018

test: make xos attaches a Mac client to a Linux daemon and a Linux client to a Mac daemon, over ssh and QUIC

a73x   2026-09-04 10:16

Commit message
test: make xos attaches a Mac client to a Linux daemon and a Linux client to a Mac daemon, over ssh and QUIC

`make vm` and `make mac` each run one operating system on both ends of
every connection, so nothing in the tree had yet graded the wire across
the OS seam. This gate does: a macOS client on a Linux daemon (entry dial
over ssh, handed off to QUIC) and `mux a` straight over QUIC with no ssh
anywhere in the path; then a Linux client on a macOS daemon the entry
dial started, on the /tmp/mux-<uid> fallback a Mac's ssh login leaves it
with, and again over QUIC — which is the leg that asks whether macOS
delivers inbound UDP to a daemon a non-GUI session started. Last, a
cross-architecture `mux d upgrade` in both directions, pinned on the
refusal's own words and on the far side's binary hashing what it hashed
before, because the preflight's `uname -m` is the whole gate and nothing
should be streamed.

Three machines and each has a job: this box orchestrates and is the Linux
client, $MUX_MAC builds its own arm64 image, $MUX_LAN is a Linux box the
Mac can reach by plain ssh. The e2e VM is on this box's private network
and the Mac cannot see it, which is why a LAN box exists at all. The
Linux box is a throwaway and gets vm.sh's scorched-earth reset; the Mac
is somebody's machine, so the reset leg REFUSES rather than killing a mux
it finds there.

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

CLAUDE.md
Old New
@@ -12,6 +12,7 @@ make check # fmt + unit tests + shell syntax + comment-claim
12 make ci # check + e2e + agent + throughput — the delivery gate 12 make ci # check + e2e + agent + throughput — the delivery gate
13 make agent soak bench throughput 13 make agent soak bench throughput
14 make vm # real user journeys against the mux-e2e VM (test/vm.sh) 14 make vm # real user journeys against the mux-e2e VM (test/vm.sh)
15 make mac xos # macOS journeys (test/mac.sh); the cross-OS gate (test/xos.sh)
15 make install # the one user binary -> ~/.local/bin, ReleaseSafe 16 make install # the one user binary -> ~/.local/bin, ReleaseSafe
16 make xversion-build xversion # cross-version gate; XVER_OLD_WORKTREE points at the old checkout (default ..) 17 make xversion-build xversion # cross-version gate; XVER_OLD_WORKTREE points at the old checkout (default ..)
17 ``` 18 ```
Makefile
Old New
@@ -13,7 +13,7 @@ MUX_TARGET ?= aarch64-macos
13 endif 13 endif
14 MUX_TARGET ?= x86_64-linux-musl 14 MUX_TARGET ?= x86_64-linux-musl
15 15
16 .PHONY: build check ci test e2e soak bench agent throughput vm coverage deps clean clean-deps xversion xversion-build install release mac-sdk mac 16 .PHONY: build check ci test e2e soak bench agent throughput vm coverage deps clean clean-deps xversion xversion-build install release mac-sdk mac xos
17 17
18 # The QUIC stack (deps/quic) is built on demand by build.zig, so no target 18 # The QUIC stack (deps/quic) is built on demand by build.zig, so no target
19 # here needs to depend on this one. It exists to make the one-time cost 19 # here needs to depend on this one. It exists to make the one-time cost
@@ -165,6 +165,21 @@ mac-sdk:
165 mac: build 165 mac: build
166 sh ./test/mac.sh 166 sh ./test/mac.sh
167 167
168 # The cross-OS gate (test/xos.sh): a macOS client on a Linux daemon and a
169 # Linux client on a macOS daemon, over ssh and over QUIC. A hand gate beside
170 # vm and mac, never inside ci — it needs two other machines, and ci stays
171 # hermetic and offline.
172 #
173 # Two builds, because the two sides of the seam are two architectures. The
174 # musl one is what gets installed on the Linux box: static, for vm's reason
175 # — that box is a VM on a host whose CPU this one did not build for. The
176 # Debug zig-out build is the Linux CLIENT and the pty fixture, which is what
177 # `make build` already produced.
178 XOSDIR ?= dist/xosgate
179 xos: build
180 $(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p $(XOSDIR)
181 ./test/xos.sh $(XOSDIR)/bin/mux zig-out/bin/mux zig-out/bin/ptyclient
182
168 # `zig build check` grades the tree; bans.sh grades the grader. The folder 183 # `zig build check` grades the tree; bans.sh grades the grader. The folder
169 # rules are the one gate whose failure mode is silence — a needle list 184 # rules are the one gate whose failure mode is silence — a needle list
170 # edited down to nothing, or a folder dropped from a rule's list, leaves a 185 # edited down to nothing, or a folder dropped from a rule's list, leaves a
README.md
Old New
@@ -727,6 +727,18 @@ tart stop mux-mac-base
727 `mux-mac-base` to `mux-mac`, boots the clone, and stops it on the way out, 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. 728 so no run can see the run before it.
729 729
730 `make mac` and `make vm` each run one operating system on both ends of every
731 connection. `make xos` is the gate that does not: it attaches a macOS client
732 to a Linux daemon and a Linux client to a macOS daemon, over the ssh handoff
733 and over QUIC, and checks that a cross-architecture `mux d upgrade` is
734 refused in both directions before any of the image is streamed. It needs two
735 other machines — the Mac named by `MUX_MAC`, which builds and installs its
736 own arm64 binary, and a Linux box named by `MUX_LAN` (default
737 `ubuntu@192.168.0.37`) that the Mac can reach by plain ssh. The Linux box is
738 a throwaway and is reset to a mux-less state at the start of every run; the
739 Mac is somebody's machine, so the gate starts nothing there that it does not
740 stop and refuses to run at all if a mux is already going.
741
730 ## How it works 742 ## How it works
731 743
732 Both ends run a real terminal engine. The daemon's is authoritative: shell 744 Both ends run a real terminal engine. The daemon's is authoritative: shell
test/xos.sh
Old New
@@ -0,0 +1,760 @@
1 #!/bin/sh
2 # test/xos.sh — the `make xos` gate: the wire, across the OS seam.
3 #
4 # `make vm` grades mux on Linux and `make mac` grades it on macOS, and both
5 # run one operating system on BOTH ends of every connection. This gate is
6 # the one that does not: a macOS client attaches to a Linux daemon and a
7 # Linux client attaches to a macOS daemon, over the ssh handoff and over
8 # QUIC, and a cross-architecture `mux d upgrade` is refused in both
9 # directions before a byte of the image moves.
10 #
11 # Three machines, and each one has a job:
12 #
13 # THIS BOX Linux x86_64, the orchestrator and the Linux CLIENT. It
14 # drives the Linux→Mac legs with this tree's own `mux` and
15 # `ptyclient`, and it builds the static-musl image the Linux
16 # daemon box installs.
17 # $MUX_MAC an Apple-silicon Mac (default `squirtle`). It holds the
18 # toolchain and the shadow SDK, so it builds its own arm64
19 # image; `make install` puts it at ~/.local/bin/mux, which is
20 # where the appended PATH of an ssh handoff looks for it. It is
21 # a person's real machine, so this script kills nothing on it
22 # and removes nothing under ~ but the directory it made.
23 # $MUX_LAN a Linux VM the Mac can reach (default ubuntu@192.168.0.37).
24 # The e2e VM is on this box's private network and the Mac
25 # cannot see it, which is the whole reason a LAN box exists.
26 # This one IS a throwaway, so it gets test/vm.sh's
27 # scorched-earth reset.
28 #
29 # The fixture is a human action, never harness code (test/vm.sh's rule):
30 # the VM is made once with eitri `vm_create {name: mux-lan, host: onyx,
31 # network: lan}`, and the Mac is somebody's laptop. What the harness owns is
32 # the reset, the install and the key.
33 #
34 # Every claim about a box is asserted by a SEPARATE ssh reading that box
35 # through test/os_oracle.sh — never by trusting the connection under test,
36 # which is the connection this gate exists to doubt ("ask the OS about the
37 # OS", CLAUDE.md).
38 set -eu
39
40 MUSL_MUX="$1" # x86_64-linux-musl ReleaseSafe: the image installed on $MUX_LAN
41 MUX="$2" # this tree's own build: the Linux client for the Linux→Mac legs
42 PTYCLIENT="$3" # the real-pty fixture, driving a client the way hands do
43
44 MAC="${MUX_MAC:-squirtle}"
45 MAC_IP="${MUX_MAC_IP:-192.168.0.47}"
46 LAN="${MUX_LAN:-ubuntu@192.168.0.37}"
47 LAN_IP=${LAN#*@}
48 # Relative to the Mac's $HOME, because that is how ssh's scp-like remote
49 # spelling (`squirtle:code/rad/mux`) resolves a path.
50 REPO="${MUX_MAC_REPO:-code/rad/mux}"
51 BRANCH=mac-journey
52 QUIC_PORT=4433
53 # 4433 as /proc/net/udp writes a wildcard bind, which is the spelling
54 # os_oracle.sh's udp_local_bound takes on BOTH arms — the Darwin arm
55 # converts it, so a leg never learns which OS it is asking.
56 QUIC_HEX=00000000:1151
57
58 # BatchMode so a broken auth fails in seconds rather than prompting a
59 # harness nobody sits in front of; accept-new so the first run records a
60 # box's host key where PLAIN ssh will find it, because mux's own entry dial
61 # spawns plain `ssh $HOST` and reads that same file.
62 SSH_OPTS="-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
63
64 # now_ms and the pid_*/file_* names the remote legs source for themselves.
65 # The harness spells time and paths through the oracle so this script would
66 # still answer if it were ever run FROM a Mac.
67 . "$(dirname "$0")/os_oracle.sh"
68
69 OK_COUNT=0
70 LEG_T0=0
71
72 leg() { LEG_T0=$(now_ms); }
73
74 ok() {
75 OK_COUNT=$((OK_COUNT + 1))
76 echo "$1 ok [$(( $(now_ms) - LEG_T0 ))ms]${2:+ ($2)}"
77 }
78
79 fail() {
80 echo "xos FAIL: $1: $2"
81 exit 1
82 }
83
84 # What every mux the HARNESS runs on a remote box runs under.
85 #
86 # XDG_STATE_HOME is isolated because every mux that attaches records its
87 # daemon in the hosts file under it (CLAUDE.md), and this gate must not
88 # write a throwaway host onto somebody's real wall. XDG_CACHE_HOME is
89 # isolated for the handoff's per-host endpoint cache and XDG_CONFIG_HOME so
90 # that no default ~/.config/mux/key can be picked up by accident — the key
91 # this run uses is named explicitly on every command line.
92 #
93 # XDG_RUNTIME_DIR is left exactly as each box's ssh login leaves it: set on
94 # Ubuntu (/run/user/1000) and UNSET on macOS, where the daemon falls back to
95 # /tmp/mux-<uid>. That fallback is what the Linux→Mac legs exercise, and the
96 # entry dial's own `mux d endpoint --start` will look nowhere else.
97 PRELUDE='
98 export XDG_STATE_HOME=$HOME/xos/state
99 export XDG_CACHE_HOME=$HOME/xos/cache
100 export XDG_CONFIG_HOME=$HOME/xos/config
101 mkdir -p $HOME/xos
102 cd $HOME
103 '
104
105 # mssh SECONDS — run the script on stdin on the Mac, under /bin/sh.
106 #
107 # /bin/sh and not the Mac's login shell (zsh): `$VAR` holding several words
108 # does not word-split in zsh, so an options string pasted into a command
109 # line there arrives as ONE argument and ssh answers "keyword batchmode
110 # extra arguments at end of line".
111 #
112 # The rule for a script handed to this: any ssh INSIDE it takes -n. The
113 # remote sh is reading its own script off stdin, and an ssh that does not
114 # close its stdin swallows the rest of that script — silently, with a 0
115 # exit, so the lines after it simply never run.
116 #
117 # The quoting rule is that there is NO quoting: the script rides this
118 # shell's stdin all the way to the remote `sh -s` and no shell in between
119 # parses it as a command line. The one thing a caller owes it is a heredoc;
120 # called with no redirect it would read the enclosing script's stdin.
121 mssh() {
122 _mt=$1
123 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
124 { printf '%s\n' "$PRELUDE"; cat; } | timeout "$_mt" ssh $SSH_OPTS "$MAC" /bin/sh -s
125 }
126
127 # lssh SECONDS — the same, onto the Linux daemon box.
128 lssh() {
129 _lt=$1
130 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
131 { printf '%s\n' "$PRELUDE"; cat; } | timeout "$_lt" ssh $SSH_OPTS "$LAN" /bin/sh -s
132 }
133
134 # ---- the local side is a hermetic laptop ------------------------------
135 # Every XDG dir mux reads, isolated. Short root because a unix socket path
136 # caps at 107 bytes.
137 SCRATCH=$(mktemp -d /tmp/muxxos.XXXXXX)
138 export XDG_STATE_HOME="$SCRATCH/st" XDG_RUNTIME_DIR="$SCRATCH/rt"
139 export XDG_CONFIG_HOME="$SCRATCH/cf" XDG_CACHE_HOME="$SCRATCH/ca"
140 mkdir -p "$XDG_STATE_HOME" "$XDG_RUNTIME_DIR" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"
141
142 # Whatever this run started, stopped — on both boxes, on every exit. The
143 # Mac's ~/.local/bin/mux is deliberately NOT removed: `make install` owns
144 # it, and a person may want the binary this run put there.
145 cleanup() {
146 _rc=$?
147 lssh 60 >/dev/null 2>&1 <<'L' || true
148 ~/.local/bin/mux d stop 2>/dev/null || true
149 sleep 0.3
150 pkill -x mux 2>/dev/null || true
151 rm -rf ~/xos
152 L
153 mssh 60 >/dev/null 2>&1 <<'M' || true
154 $HOME/.local/bin/mux d stop 2>/dev/null || true
155 rm -rf $HOME/xos
156 M
157 rm -rf "$SCRATCH"
158 exit "$_rc"
159 }
160 trap cleanup EXIT
161
162 # ---- leg 1: push — the Mac builds THIS commit --------------------------
163 leg
164 HEAD_SHA=$(git rev-parse HEAD)
165 PUSHED=no
166 HAVE=$(mssh 60 <<H || true
167 cd \$HOME/$REPO 2>/dev/null || exit 0
168 git rev-parse -q --verify refs/heads/$BRANCH 2>/dev/null || true
169 H
170 )
171 if [ "$HAVE" != "$HEAD_SHA" ]; then
172 # A push cannot update the branch the remote has checked out, and the
173 # run before this one left it checked out. Detaching first is what makes
174 # a re-run work; it changes no commit and no file.
175 mssh 60 >/dev/null <<H || fail push "cannot detach $MAC:$REPO before the push"
176 set -e
177 cd \$HOME/$REPO
178 if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then
179 git checkout -q --detach
180 fi
181 H
182 # --force because this branch is a scratch mirror of whatever this tree
183 # is and never shared history: a local amend or rebase leaves the Mac's
184 # ref on a commit the new HEAD does not descend from, and a plain push
185 # would be rejected as a non-fast-forward for the rest of the branch's
186 # life.
187 git push --force "$MAC:$REPO" "HEAD:refs/heads/$BRANCH" >/dev/null 2>&1 ||
188 fail push "git push $MAC:$REPO HEAD:refs/heads/$BRANCH was refused"
189 PUSHED=yes
190 fi
191 mssh 60 >/dev/null <<H || fail push "the Mac could not check out $BRANCH"
192 set -e
193 cd \$HOME/$REPO
194 git checkout -q $BRANCH
195 git reset -q --hard $BRANCH
196 H
197 MAC_SHA_GIT=$(mssh 60 <<H
198 cd \$HOME/$REPO && git rev-parse HEAD
199 H
200 )
201 [ "$MAC_SHA_GIT" = "$HEAD_SHA" ] ||
202 fail push "the Mac is at $MAC_SHA_GIT, this tree is at $HEAD_SHA"
203 ok push "$MAC:$REPO on $BRANCH at $(git rev-parse --short HEAD), pushed=$PUSHED"
204
205 # ---- leg 2: build — one image per architecture -------------------------
206 # The Mac builds its own arm64 binary and installs it where an ssh
207 # handoff's appended PATH finds it. The shim PATH is required for the link
208 # and Homebrew is not on a non-interactive PATH, so both are spelled here.
209 leg
210 BOUT=$(mssh 2400 <<H
211 cd \$HOME/$REPO || { echo "RC_BUILD=90"; exit 0; }
212 export PATH=\$HOME/$REPO/deps/mac-sdk/bin:/opt/homebrew/bin:\$PATH
213 make build >/tmp/xos-build.log 2>&1; echo "RC_BUILD=\$?"
214 make install >/tmp/xos-install.log 2>&1; echo "RC_INSTALL=\$?"
215 H
216 )
217 for _step in BUILD INSTALL; do
218 _rc=$(printf '%s\n' "$BOUT" | sed -n "s/^RC_$_step=//p")
219 [ "$_rc" = "0" ] && continue
220 _log=$(printf '%s' "$_step" | tr 'A-Z' 'a-z')
221 # The verdict first and the reading under it, test/vm.sh's shape: a
222 # reader scrolling back finds the sentence that says what broke, not the
223 # tail of somebody else's build log.
224 echo "xos FAIL: build: make $_log on $MAC exited ${_rc:-<no answer>}"
225 mssh 60 <<H | sed 's/^/ /' || true
226 tail -30 /tmp/xos-$_log.log 2>/dev/null || true
227 H
228 exit 1
229 done
230 # The oracle goes beside the binary rather than being read out of the repo,
231 # so both remote boxes answer through the same path and a leg never has to
232 # know which one it is talking to.
233 MAC_INFO=$(mssh 120 <<H
234 set -e
235 mkdir -p \$HOME/xos
236 cp \$HOME/$REPO/test/os_oracle.sh \$HOME/xos/os_oracle.sh
237 . \$HOME/xos/os_oracle.sh
238 echo "ver=\$(\$HOME/.local/bin/mux --version)"
239 echo "sha=\$(sha256_of \$HOME/.local/bin/mux)"
240 echo "exe=\$(real_path \$HOME/.local/bin/mux)"
241 echo "arch=\$(uname -m)"
242 H
243 )
244 MAC_VER=$(printf '%s\n' "$MAC_INFO" | sed -n 's/^ver=//p')
245 MAC_BIN_SHA=$(printf '%s\n' "$MAC_INFO" | sed -n 's/^sha=//p')
246 MAC_EXE=$(printf '%s\n' "$MAC_INFO" | sed -n 's/^exe=//p')
247 MAC_ARCH=$(printf '%s\n' "$MAC_INFO" | sed -n 's/^arch=//p')
248 WANT_VER=$("$MUX" --version)
249 [ -n "$MAC_VER" ] && [ "$MAC_VER" = "$WANT_VER" ] ||
250 fail build "$MAC answers '$MAC_VER', this tree built '$WANT_VER'"
251 [ "$MAC_ARCH" = "arm64" ] ||
252 fail build "$MAC says it is '$MAC_ARCH'; this gate is about an arm64 Mac"
253 [ -n "$MAC_BIN_SHA" ] || fail build "$MAC would not hash its installed mux"
254 ok build "$MAC installed an $MAC_ARCH '$MAC_VER' at $MAC_EXE"
255
256 # ---- leg 3: reset — a mux-less Linux box, and a Mac left alone ---------
257 leg
258 # The Mac first, and it is a REFUSAL and not a reset. This is a person's
259 # machine: a stray mux here is theirs, and the Linux→Mac legs must meet the
260 # daemon THEY start, not one already running with unknown flags.
261 MPRE=$(mssh 60 <<'M'
262 echo "uid=$(id -u)"
263 echo "muxes=$(pgrep -x mux 2>/dev/null | wc -l | tr -d ' ')"
264 [ -e /tmp/mux-$(id -u) ] && echo "rt=present" || echo "rt=absent"
265 ifconfig 2>/dev/null | sed -n 's/^[[:space:]]*inet \([0-9.]*\) .*/ip=\1/p'
266 M
267 )
268 MAC_UID=$(printf '%s\n' "$MPRE" | sed -n 's/^uid=//p')
269 [ -n "$MAC_UID" ] || fail reset "$MAC would not say its uid"
270 printf '%s\n' "$MPRE" | grep -qx 'muxes=0' || {
271 echo "xos FAIL: reset: $MAC already runs a mux, and this gate will not kill it."
272 echo " Stop it there (mux d stop) or run this against another Mac."
273 exit 1; }
274 printf '%s\n' "$MPRE" | grep -qx 'rt=absent' ||
275 fail reset "$MAC already has /tmp/mux-$MAC_UID; the daemon this gate meets must be the one it starts"
276 # The address the QUIC legs dial, asserted against the Mac's own interface
277 # list rather than assumed from a variable: a stale MUX_MAC_IP would make
278 # leg 8 grade some other machine's silence.
279 printf '%s\n' "$MPRE" | grep -qx "ip=$MAC_IP" ||
280 fail reset "$MAC does not hold $MAC_IP; set MUX_MAC_IP. It answered: $(printf '%s' "$MPRE" | tr '\n' ' ')"
281 MAC_SOCK="/tmp/mux-$MAC_UID/muxd.sock"
282
283 # The Linux box IS a throwaway, so it gets test/vm.sh's scorched earth. The
284 # reset is the hermeticity: each run starts from the mux-less box a new user
285 # has. The two standing arrangements it (re)makes:
286 # linger — without it logind may tear down /run/user/1000 (the daemon's
287 # socket dir) between ssh sessions and reap the daemon with it.
288 # PATH — an ssh handoff runs a non-interactive bash whose PATH lacks
289 # ~/.local/bin; the line goes ABOVE bashrc's interactivity
290 # return, where Debian's ssh-aware bash reads it.
291 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
292 LAN_ARCH=$(timeout 20 ssh $SSH_OPTS "$LAN" 'uname -m') ||
293 fail reset "cannot ssh to $LAN — is the VM up, and has its DHCP lease moved?"
294 [ "$LAN_ARCH" = "x86_64" ] ||
295 fail reset "$LAN is $LAN_ARCH, and this gate installs an x86_64-musl build there"
296 lssh 120 >/dev/null <<'L' || fail reset "the scorched-earth reset was refused on the Linux box"
297 set -eu
298 pkill -x mux 2>/dev/null || true
299 sleep 0.3
300 pkill -9 -x mux 2>/dev/null || true
301 rm -rf ~/.local/bin/mux ~/.local/state/mux ~/.cache/mux ~/.config/mux ~/xos
302 rm -rf "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"/mux*
303 sudo -n loginctl enable-linger "$(id -un)"
304 grep -q '^export PATH="$HOME/.local/bin' ~/.bashrc 2>/dev/null || \
305 sed -i '1i export PATH="$HOME/.local/bin:$PATH"' ~/.bashrc
306 mkdir -p ~/.local/bin ~/xos
307 L
308 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
309 scp -q $SSH_OPTS "$MUSL_MUX" "$LAN":.local/bin/mux ||
310 fail reset "could not copy the musl build to $LAN"
311 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
312 scp -q $SSH_OPTS "$(dirname "$0")/os_oracle.sh" "$LAN":xos/os_oracle.sh ||
313 fail reset "could not copy the oracle to $LAN"
314 LAN_BIN_SHA=$(sha256_of "$MUSL_MUX")
315 LINFO=$(lssh 60 <<'L'
316 . ~/xos/os_oracle.sh
317 echo "ver=$(~/.local/bin/mux --version)"
318 echo "sha=$(sha256_of ~/.local/bin/mux)"
319 echo "rtd=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
320 L
321 )
322 LAN_VER=$(printf '%s\n' "$LINFO" | sed -n 's/^ver=//p')
323 LAN_GOT_SHA=$(printf '%s\n' "$LINFO" | sed -n 's/^sha=//p')
324 LAN_RTD=$(printf '%s\n' "$LINFO" | sed -n 's/^rtd=//p')
325 [ "$LAN_VER" = "$WANT_VER" ] ||
326 fail reset "$LAN answers '$LAN_VER', this tree built '$WANT_VER'"
327 [ "$LAN_GOT_SHA" = "$LAN_BIN_SHA" ] ||
328 fail reset "$LAN holds a mux hashing $LAN_GOT_SHA, the pushed image is $LAN_BIN_SHA"
329 LAN_SOCK="$LAN_RTD/muxd.sock"
330 ok reset "$LAN reset to a mux-less box and given '$LAN_VER'; $MAC left running nothing"
331
332 # ---- leg 4: keys — one PSK for the run, never the user's ---------------
333 # `mux d keygen` always writes the default path, so the isolated
334 # XDG_CONFIG_HOME above is what puts it in the scratch tree. It is copied to
335 # ~/xos/key on both boxes and named on every QUIC command line; the Mac's
336 # own ~/.config/mux/key is never read and never written.
337 leg
338 "$MUX" d keygen >/dev/null 2>&1 || fail keys "mux d keygen refused under $XDG_CONFIG_HOME"
339 cp "$XDG_CONFIG_HOME/mux/key" "$SCRATCH/key"
340 chmod 600 "$SCRATCH/key"
341 [ "$(file_size "$SCRATCH/key")" = "32" ] ||
342 fail keys "the generated key is $(file_size "$SCRATCH/key") bytes, want 32"
343 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
344 scp -q $SSH_OPTS "$SCRATCH/key" "$LAN":xos/key || fail keys "could not install the key on $LAN"
345 # shellcheck disable=SC2086 # SSH_OPTS is words on purpose
346 scp -q $SSH_OPTS "$SCRATCH/key" "$MAC":xos/key || fail keys "could not install the key on $MAC"
347 # 0600 on both, because quic.Key.load refuses a group- or other-readable key
348 # exactly as ssh refuses a private key, and scp does not carry the mode.
349 KMODES=$(
350 lssh 30 <<'L'
351 . ~/xos/os_oracle.sh
352 chmod 600 ~/xos/key
353 echo "lan=$(file_mode ~/xos/key)"
354 L
355 mssh 30 <<'M'
356 . $HOME/xos/os_oracle.sh
357 chmod 600 $HOME/xos/key
358 echo "mac=$(file_mode $HOME/xos/key)"
359 M
360 )
361 printf '%s\n' "$KMODES" | grep -qx 'lan=600' ||
362 fail keys "the key on $LAN is not mode 600: $KMODES"
363 printf '%s\n' "$KMODES" | grep -qx 'mac=600' ||
364 fail keys "the key on $MAC is not mode 600: $KMODES"
365 ok keys "one 32-byte PSK at ~/xos/key, mode 600 on both boxes"
366
367 # ---- leg 5: mac-to-linux — a Mac client on a Linux daemon --------------
368 # The daemon carries a QUIC arm, so the Mac's entry dial is the whole
369 # product path: plain ssh to fetch the announce, then the session moves onto
370 # QUIC. MUX_SHELL_INTEGRATION=1 because leg 6 reads a real exit code off
371 # this same daemon and marks are opt-in.
372 leg
373 lssh 90 >/dev/null <<L || fail mac-to-linux "mux d start -d --quic was refused on $LAN"
374 MUX_SHELL_INTEGRATION=1 ~/.local/bin/mux d start -d --quic 0.0.0.0:$QUIC_PORT --key ~/xos/key
375 L
376 LPID=$(lssh 30 <<'L'
377 pgrep -x mux | head -1
378 L
379 )
380 [ -n "$LPID" ] || fail mac-to-linux "no mux process on $LAN after mux d start -d"
381 # Bound BEFORE the dial, asked of /proc and not of the daemon: a leg that
382 # read "no answer" from the Mac could otherwise be blaming the network for a
383 # listener that was never up.
384 lssh 60 >/dev/null <<L || fail mac-to-linux "daemon $LPID binds no UDP $QUIC_PORT on $LAN"
385 . ~/xos/os_oracle.sh
386 _i=0
387 while [ \$_i -lt 50 ]; do
388 udp_local_bound $QUIC_HEX && exit 0
389 sleep 0.2; _i=\$((_i + 1))
390 done
391 exit 1
392 L
393
394 # The marker carries the SESSION shell's own `uname -m` and its own \$\$, so
395 # `XOS-M2L-x86_64-` painted on a Mac's screen is the cross-OS fact itself:
396 # the bytes were produced by a shell only the Linux box's process table can
397 # account for.
398 M2L=$(mssh 240 <<M
399 cd \$HOME/xos
400 \$HOME/$REPO/zig-out/bin/ptyclient --cols 80 --rows 24 \
401 --out \$HOME/xos/m2l.cap --err \$HOME/xos/m2l.err -- \
402 \$HOME/.local/bin/mux $LAN <<'PC'
403 expect \\x1b[?1049h 30000
404 settle 900 20000
405 send echo XOS-M2L-\$(uname -m)-\$\$\n
406 expect XOS-M2L-x86_64- 25000
407 settle 800 15000
408 send \\x1cd
409 waitexit 15000
410 PC
411 echo "PTYRC=\$?"
412 M
413 ) || fail mac-to-linux "$MAC would not run ptyclient"
414 printf '%s\n' "$M2L" | grep -qx 'PTYRC=0' || {
415 echo "xos FAIL: mac-to-linux: the Mac's client never painted XOS-M2L-x86_64-: $M2L"
416 mssh 60 <<'M' | sed 's/^/ /' || true
417 cat $HOME/xos/m2l.err 2>/dev/null
418 M
419 exit 1; }
420 M2L_READ=$(mssh 60 <<'M'
421 echo "pid=$(sed -n 's/.*XOS-M2L-x86_64-\([0-9][0-9]*\).*/\1/p' $HOME/xos/m2l.cap | head -1)"
422 grep -c 'attaching over ssh' $HOME/xos/m2l.err 2>/dev/null || true
423 M
424 )
425 M2L_PID=$(printf '%s\n' "$M2L_READ" | sed -n 's/^pid=//p')
426 [ -n "$M2L_PID" ] || fail mac-to-linux "the Mac's capture carries no shell pid: $M2L_READ"
427 # The handoff prints this one line and only when QUIC was tried and lost, so
428 # its ABSENCE is the pin that the session rode QUIC. The daemon's bound port
429 # above says the arm existed to move onto.
430 printf '%s\n' "$M2L_READ" | grep -qx '0' ||
431 fail mac-to-linux "the Mac's dial fell back to the ssh pipe; it said: $M2L_READ"
432 lssh 60 >/dev/null <<L || fail mac-to-linux "shell $M2L_PID is not a child of daemon $LPID on $LAN"
433 . ~/xos/os_oracle.sh
434 pid_children $LPID | grep -qx $M2L_PID
435 L
436 # The detach was a goodbye and not a tear: what the Mac typed is still on
437 # the Linux box's grid.
438 lssh 60 >/dev/null <<'L' || fail mac-to-linux "the session did not survive the Mac's detach"
439 ~/.local/bin/mux d dump | grep -q XOS-M2L-x86_64-
440 L
441 ok mac-to-linux "a macOS client typed into shell $M2L_PID under daemon $LPID on $LAN, over QUIC"
442
443 # ---- leg 6: mac-to-linux-quic — no ssh anywhere in the path ------------
444 # The Darwin wolfSSL build's PSK handshake against the musl build's, in one
445 # round trip, joining the session leg 5 made. `mux a` never creates.
446 leg
447 AQ=$(mssh 120 <<M
448 MUX_KEY_FILE=\$HOME/xos/key \$HOME/.local/bin/mux a run --quic $LAN_IP:$QUIC_PORT \
449 --key \$HOME/xos/key --timeout 20000 'echo m2lq-\$\$'
450 M
451 ) || fail mac-to-linux-quic "mux a run --quic answered nothing from $MAC: $AQ"
452 case "$AQ" in
453 *'"mechanism":"marks"'*) ;;
454 *) fail mac-to-linux-quic "no marks mechanism in the reply: $AQ" ;;
455 esac
456 case "$AQ" in
457 *'"exit_code":0'*) ;;
458 *) fail mac-to-linux-quic "no clean exit code in the reply: $AQ" ;;
459 esac
460 AQ_PID=$(printf '%s\n' "$AQ" | sed -n 's/.*m2lq-\([0-9][0-9]*\).*/\1/p')
461 [ -n "$AQ_PID" ] || fail mac-to-linux-quic "the reply carries no shell pid: $AQ"
462 lssh 60 >/dev/null <<L || fail mac-to-linux-quic "shell $AQ_PID is not a child of daemon $LPID"
463 . ~/xos/os_oracle.sh
464 pid_children $LPID | grep -qx $AQ_PID
465 L
466 ok mac-to-linux-quic "mux a over QUIC ran shell $AQ_PID under $LPID, marks, exit 0"
467
468 # ---- leg 7: linux-to-mac — the entry dial starts the Mac's daemon ------
469 # `mux $MAC` from here is the ONLY thing in this gate allowed to start a
470 # daemon on the Mac: `mux d endpoint --start` is the asked dial's alone. The
471 # Mac's ssh login has no XDG_RUNTIME_DIR, so the daemon it starts binds the
472 # /tmp/mux-<uid> fallback — the Darwin path, exercised by a real `mux HOST`
473 # rather than by a test that sets a variable.
474 leg
475 set +e
476 timeout 120 "$PTYCLIENT" --cols 80 --rows 24 \
477 --out "$SCRATCH/l2m.cap" --err "$SCRATCH/l2m.err" -- "$MUX" "$MAC" \
478 > "$SCRATCH/l2m.log" 2>&1 <<'EOF'
479 expect \x1b[?1049h 30000
480 settle 900 20000
481 send echo XOS-L2M-$(uname -m)-$$\n
482 expect XOS-L2M-arm64- 25000
483 settle 800 15000
484 send \x1cd
485 waitexit 15000
486 EOF
487 RC=$?
488 set -e
489 [ "$RC" -eq 0 ] || {
490 echo "xos FAIL: linux-to-mac: ptyclient exited $RC (did the dial or the remote start refuse?):"
491 sed 's/^/ /' "$SCRATCH/l2m.log" 2>/dev/null || true
492 sed 's/^/ /' "$SCRATCH/l2m.err" 2>/dev/null || true
493 exit 1; }
494 L2M_PID=$(sed -n 's/.*XOS-L2M-arm64-\([0-9][0-9]*\).*/\1/p' "$SCRATCH/l2m.cap" | head -1)
495 [ -n "$L2M_PID" ] || fail linux-to-mac "the capture carries no shell pid"
496
497 MSTATE=$(mssh 90 <<M
498 . \$HOME/xos/os_oracle.sh
499 echo "pid=\$(pgrep -x mux | head -1)"
500 echo "n=\$(pgrep -x mux | wc -l | tr -d ' ')"
501 echo "announce=\$(\$HOME/.local/bin/mux d endpoint 2>&1 | head -1)"
502 echo "want=\$(real_path \$HOME/.local/bin/mux)"
503 udp_local_bound $QUIC_HEX && echo "udp=bound" || echo "udp=none"
504 M
505 )
506 MPID=$(printf '%s\n' "$MSTATE" | sed -n 's/^pid=//p')
507 MNPROC=$(printf '%s\n' "$MSTATE" | sed -n 's/^n=//p')
508 MANN=$(printf '%s\n' "$MSTATE" | sed -n 's/^announce=//p')
509 MWANT=$(printf '%s\n' "$MSTATE" | sed -n 's/^want=//p')
510 [ -n "$MPID" ] || fail linux-to-mac "no mux on $MAC after the entry dial: $MSTATE"
511 [ "$MNPROC" = "1" ] ||
512 fail linux-to-mac "$MAC runs $MNPROC mux processes after the detach, want 1 (the daemon): $MSTATE"
513 mssh 60 >/dev/null <<M || fail linux-to-mac "daemon $MPID does not hold $MAC_SOCK"
514 . \$HOME/xos/os_oracle.sh
515 pid_holds_unix_sock $MPID $MAC_SOCK
516 M
517 # Both sides canonical, and both through the oracle: pid_exe answers what
518 # lsof reports, which resolves every symlink on the way, so a $HOME reached
519 # through one would fail a correct exec unless the expected path is resolved
520 # the same way.
521 MIS=$(mssh 60 <<M
522 . \$HOME/xos/os_oracle.sh
523 pid_exe $MPID
524 M
525 )
526 [ -n "$MWANT" ] && [ "$MIS" = "$MWANT" ] ||
527 fail linux-to-mac "daemon $MPID execs '$MIS', not the installed '$MWANT'"
528 mssh 60 >/dev/null <<M || fail linux-to-mac "shell $L2M_PID is not a child of daemon $MPID"
529 . \$HOME/xos/os_oracle.sh
530 pid_children $MPID | grep -qx $L2M_PID
531 M
532 # WHICH transport carried it, read off the announce rather than assumed. The
533 # entry dial started this daemon bare — no --quic, no key — so what it says
534 # is `endpoint none`, the handoff's explicit "no coordinates", and the
535 # session stays on the ssh pipe. There is no positive line for that case
536 # (the client only speaks up when QUIC was tried and lost), so the pin is
537 # three facts: the daemon offers no arm, it binds no UDP port, and the
538 # client printed no fallback sentence. If a future daemon does announce an
539 # arm here, this leg says so instead of quietly grading the other transport.
540 # An `if` and not `grep ... && fail`: an AND-OR list whose first command
541 # fails is the ordinary case here, and reading it as the leg's verdict is
542 # the wrong way round.
543 case "$MANN" in
544 "endpoint none")
545 printf '%s\n' "$MSTATE" | grep -qx 'udp=none' ||
546 fail linux-to-mac "$MAC announces no endpoint yet binds UDP $QUIC_PORT: $MSTATE"
547 if grep -q 'attaching over ssh' "$SCRATCH/l2m.err"; then
548 fail linux-to-mac "the client fell back from a QUIC arm the daemon says it has not got"
549 fi
550 L2M_WIRE="ssh handoff (the daemon announced 'endpoint none')" ;;
551 'endpoint '*)
552 printf '%s\n' "$MSTATE" | grep -qx 'udp=bound' ||
553 fail linux-to-mac "$MAC announces '$MANN' but binds no UDP $QUIC_PORT: $MSTATE"
554 if grep -q 'attaching over ssh' "$SCRATCH/l2m.err"; then
555 fail linux-to-mac "$MAC offered QUIC ('$MANN') and the dial fell back to ssh"
556 fi
557 L2M_WIRE="QUIC (the daemon announced '$MANN')" ;;
558 *)
559 fail linux-to-mac "$MAC's daemon answers no announce this leg can read: '$MANN'" ;;
560 esac
561 ok linux-to-mac "shell $L2M_PID under daemon $MPID on $MAC_SOCK, over $L2M_WIRE"
562
563 # ---- leg 8: linux-to-mac-quic — inbound UDP to a Mac daemon ------------
564 # The question this leg asks and no other does: does macOS deliver inbound
565 # UDP $QUIC_PORT to a daemon a NON-GUI ssh session started? If the
566 # application firewall eats it, the dial times out in silence and this leg
567 # fails with the client's own words — which is a fact about the platform,
568 # not a harness problem to loosen away.
569 leg
570 mssh 60 >/dev/null <<'M' || fail linux-to-mac-quic "mux d stop was refused on $MAC"
571 $HOME/.local/bin/mux d stop
572 M
573 mssh 60 >/dev/null <<M || fail linux-to-mac-quic "the bare daemon $MPID survives mux d stop by 3 s"
574 . \$HOME/xos/os_oracle.sh
575 _i=0
576 while [ \$_i -lt 30 ]; do
577 pid_alive $MPID || exit 0
578 sleep 0.1; _i=\$((_i + 1))
579 done
580 exit 1
581 M
582 mssh 90 >/dev/null <<M || fail linux-to-mac-quic "mux d start -d --quic was refused on $MAC"
583 MUX_SHELL_INTEGRATION=1 \$HOME/.local/bin/mux d start -d --quic 0.0.0.0:$QUIC_PORT --key \$HOME/xos/key
584 M
585 MPID2=$(mssh 30 <<'M'
586 pgrep -x mux | head -1
587 M
588 )
589 [ -n "$MPID2" ] || fail linux-to-mac-quic "no mux on $MAC after the QUIC start"
590 [ "$MPID2" != "$MPID" ] ||
591 fail linux-to-mac-quic "$MAC still runs the pre-stop daemon $MPID; the restart did not happen"
592 mssh 60 >/dev/null <<M || fail linux-to-mac-quic "daemon $MPID2 binds no UDP $QUIC_PORT on $MAC"
593 . \$HOME/xos/os_oracle.sh
594 _i=0
595 while [ \$_i -lt 50 ]; do
596 udp_local_bound $QUIC_HEX && exit 0
597 sleep 0.2; _i=\$((_i + 1))
598 done
599 exit 1
600 M
601
602 # A real pty attach first, because `mux a` joins and never creates.
603 export MUX_KEY_FILE="$SCRATCH/key"
604 set +e
605 timeout 120 "$PTYCLIENT" --cols 80 --rows 24 \
606 --out "$SCRATCH/l2mq.cap" --err "$SCRATCH/l2mq.err" -- \
607 "$MUX" "quic://$MAC_IP:$QUIC_PORT" \
608 > "$SCRATCH/l2mq.log" 2>&1 <<'EOF'
609 expect \x1b[?1049h 30000
610 settle 900 20000
611 send echo XOS-L2MQ-$(uname -m)-$$\n
612 expect XOS-L2MQ-arm64- 25000
613 settle 800 15000
614 send \x1cd
615 waitexit 15000
616 EOF
617 RC=$?
618 set -e
619 [ "$RC" -eq 0 ] || {
620 echo "xos FAIL: linux-to-mac-quic: the QUIC attach to $MAC_IP:$QUIC_PORT exited $RC."
621 echo " This is the leg that asks whether macOS delivers inbound UDP"
622 echo " $QUIC_PORT to a daemon a non-GUI ssh session started. The dial said:"
623 sed 's/^/ /' "$SCRATCH/l2mq.log" 2>/dev/null || true
624 sed 's/^/ /' "$SCRATCH/l2mq.err" 2>/dev/null || true
625 exit 1; }
626 L2MQ_PID=$(sed -n 's/.*XOS-L2MQ-arm64-\([0-9][0-9]*\).*/\1/p' "$SCRATCH/l2mq.cap" | head -1)
627 [ -n "$L2MQ_PID" ] || fail linux-to-mac-quic "the QUIC capture carries no shell pid"
628 mssh 60 >/dev/null <<M || fail linux-to-mac-quic "shell $L2MQ_PID is not a child of daemon $MPID2"
629 . \$HOME/xos/os_oracle.sh
630 pid_children $MPID2 | grep -qx $L2MQ_PID
631 M
632 AQ2=$(set +e; timeout 60 "$MUX" a run --quic "$MAC_IP:$QUIC_PORT" --key "$SCRATCH/key" \
633 --timeout 20000 'echo l2mq-$$'; echo "RC=$?")
634 case "$AQ2" in
635 *RC=0*) ;;
636 *) fail linux-to-mac-quic "mux a run --quic to $MAC did not answer: $AQ2" ;;
637 esac
638 case "$AQ2" in
639 *'"mechanism":"marks"'*) ;;
640 *) fail linux-to-mac-quic "no marks mechanism in the reply from $MAC: $AQ2" ;;
641 esac
642 AQ2_PID=$(printf '%s\n' "$AQ2" | sed -n 's/.*l2mq-\([0-9][0-9]*\).*/\1/p')
643 [ -n "$AQ2_PID" ] || fail linux-to-mac-quic "the reply carries no shell pid: $AQ2"
644 mssh 60 >/dev/null <<M || fail linux-to-mac-quic "shell $AQ2_PID is not a child of daemon $MPID2"
645 . \$HOME/xos/os_oracle.sh
646 pid_children $MPID2 | grep -qx $AQ2_PID
647 M
648 unset MUX_KEY_FILE
649 ok linux-to-mac-quic "macOS delivered inbound UDP $QUIC_PORT: shells $L2MQ_PID and $AQ2_PID under $MPID2"
650
651 # ---- leg 9: upgrade-refused — the preflight is uname, both ways --------
652 # `uname -m` is the FIRST line of the preflight and the whole gate: the
653 # image is never opened and nothing is streamed. The pin is therefore both
654 # the refusal's own words and the installed binary's hash on the far side,
655 # unchanged from what legs 2 and 3 put there.
656 leg
657 set +e
658 timeout 120 "$MUX" d upgrade "$MAC" --allow-same-version > "$SCRATCH/up_mac.out" 2>&1
659 RC=$?
660 set -e
661 [ "$RC" -ne 0 ] ||
662 fail upgrade-refused "mux d upgrade $MAC exited 0; an x86_64 image must not reach an arm64 box"
663 grep -q "is arm64 and this image is x86_64; refusing to push a binary that cannot run there" \
664 "$SCRATCH/up_mac.out" || {
665 echo "xos FAIL: upgrade-refused: the Linux→Mac refusal is not the arch line; it said:"
666 sed 's/^/ /' "$SCRATCH/up_mac.out"
667 exit 1; }
668
669 # From the Mac, the mirror. The image there is aarch64 and the far side says
670 # x86_64, so the two words differ in BOTH spelling and machine — which is
671 # what makes this the mirror image and not the same test twice.
672 UPL=$(mssh 180 <<M
673 \$HOME/.local/bin/mux d upgrade $LAN --allow-same-version 2>&1
674 echo "RC=\$?"
675 M
676 )
677 case "$UPL" in
678 *RC=0*) fail upgrade-refused "mux d upgrade $LAN from $MAC exited 0; an aarch64 image must not reach an x86_64 box" ;;
679 esac
680 case "$UPL" in
681 *"is x86_64 and this image is aarch64; refusing to push a binary that cannot run there"*) ;;
682 *) fail upgrade-refused "the Mac→Linux refusal is not the arch line; it said: $UPL" ;;
683 esac
684
685 POST=$(
686 mssh 60 <<M
687 . \$HOME/xos/os_oracle.sh
688 echo "macsha=\$(sha256_of \$HOME/.local/bin/mux)"
689 echo "macpid=\$(pgrep -x mux | head -1)"
690 echo "macexe=\$(pid_exe \$(pgrep -x mux | head -1))"
691 M
692 lssh 60 <<L
693 . ~/xos/os_oracle.sh
694 echo "lansha=\$(sha256_of ~/.local/bin/mux)"
695 echo "lanpid=\$(pgrep -x mux | head -1)"
696 echo "lanexe=\$(pid_exe $LPID)"
697 L
698 )
699 printf '%s\n' "$POST" | grep -qx "macsha=$MAC_BIN_SHA" ||
700 fail upgrade-refused "$MAC's installed mux changed across the refusal: $POST"
701 printf '%s\n' "$POST" | grep -qx "lansha=$LAN_BIN_SHA" ||
702 fail upgrade-refused "$LAN's installed mux changed across the refusal: $POST"
703 printf '%s\n' "$POST" | grep -qx "macpid=$MPID2" ||
704 fail upgrade-refused "$MAC's daemon is no longer $MPID2: $POST"
705 printf '%s\n' "$POST" | grep -qx "lanpid=$LPID" ||
706 fail upgrade-refused "$LAN's daemon is no longer $LPID: $POST"
707 printf '%s\n' "$POST" | grep -qx "macexe=$MAC_EXE" ||
708 fail upgrade-refused "$MAC's daemon execs something else now: $POST"
709 ok upgrade-refused "both pushes refused on uname alone; both images and both daemons untouched"
710
711 # ---- leg 10: stop — and each box's socket goes with its daemon ---------
712 leg
713 mssh 60 >/dev/null <<'M' || fail stop "mux d stop was refused on $MAC"
714 $HOME/.local/bin/mux d stop
715 M
716 lssh 60 >/dev/null <<'L' || fail stop "mux d stop was refused on $LAN"
717 ~/.local/bin/mux d stop
718 L
719 MEND=$(mssh 60 <<M
720 . \$HOME/xos/os_oracle.sh
721 _i=0
722 while [ \$_i -lt 30 ]; do
723 pid_alive $MPID2 || break
724 sleep 0.1; _i=\$((_i + 1))
725 done
726 pid_alive $MPID2 && echo "alive=yes" || echo "alive=no"
727 [ -e $MAC_SOCK ] && echo "sock=present" || echo "sock=gone"
728 M
729 )
730 printf '%s\n' "$MEND" | grep -qx 'alive=no' ||
731 fail stop "$MAC's daemon $MPID2 survives mux d stop by 3 s: $MEND"
732 printf '%s\n' "$MEND" | grep -qx 'sock=gone' ||
733 fail stop "$MAC_SOCK outlives the daemon that bound it: $MEND"
734 LEND=$(lssh 60 <<L
735 . ~/xos/os_oracle.sh
736 _i=0
737 while [ \$_i -lt 30 ]; do
738 pid_alive $LPID || break
739 sleep 0.1; _i=\$((_i + 1))
740 done
741 pid_alive $LPID && echo "alive=yes" || echo "alive=no"
742 [ -e $LAN_SOCK ] && echo "sock=present" || echo "sock=gone"
743 L
744 )
745 printf '%s\n' "$LEND" | grep -qx 'alive=no' ||
746 fail stop "$LAN's daemon $LPID survives mux d stop by 3 s: $LEND"
747 printf '%s\n' "$LEND" | grep -qx 'sock=gone' ||
748 fail stop "$LAN_SOCK outlives the daemon that bound it: $LEND"
749 ok stop "both daemons gone within 3 s, each taking its own socket with it"
750
751 # ---- the pin ----------------------------------------------------------
752 # A literal, test/vm.sh's and test/mac.sh's style: adding a leg means
753 # editing this by hand, so a leg that stops running cannot pass by not being
754 # counted.
755 [ "$OK_COUNT" = "10" ] || {
756 echo "xos FAIL: $OK_COUNT legs ran, the pin says 10 —"
757 echo " a leg was added (update the pin) or silently lost"
758 exit 1
759 }
760 echo "xos OK ($OK_COUNT legs: $MAC ($MAC_ARCH) and $LAN ($LAN_ARCH), each the other's client)"