a73x

4bcb5c2a

test: the M18 wall leg is one daemon whose sessions are its tiles

a73x   2026-08-29 12:14

Commit message
test: the M18 wall leg is one daemon whose sessions are its tiles

It passed `--sock PATH#a` on argv, which the host grammar refuses. The
claim is unchanged and now needs no #SESSION at all: one host line, three
sessions, three tiles, no crosstalk. The id lookup both hub legs need
moves into e2e_lib as tile_id_of/tiles_shape.

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

test/e2e_05_session.sh
Old New
@@ -206,22 +206,19 @@ D18PID=""
206 ok "two sessions on one daemon are two shells; one dies without the other" 206 ok "two sessions on one daemon are two shells; one dies without the other"
207 ok "a daemon outlives its last session and is born into again" 207 ok "a daemon outlives its last session and is born into again"
208 208
209 # --- M18 on the wall: one socket, two tiles, two sessions --------------- 209 # --- M18 on the wall: one daemon, two sessions, two tiles ---------------
210 # What the milestone was FOR (decision 2): the wall showing the same host 210 # What the milestone was FOR (decision 2): the wall showing the same host
211 # twice. Both tiles name the same socket path and differ only in the 211 # twice. ONE host line names the daemon and its two sessions are two
212 # `#NAME` suffix, so the hub dials it twice and the two connections are 212 # tiles, so the hub dials it twice and the two connections are two
213 # two sessions — the browser's spelling of everything the block above 213 # sessions — the browser's spelling of everything the block above proved
214 # proved with a CLI. 214 # with a CLI.
215 start_daemon "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" --shell /bin/sh 215 start_daemon "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" --shell /bin/sh
216 D19PID=$DPID 216 D19PID=$DPID
217 "$MUX" web --sock "$SOCK22#a" --sock "$SOCK22#b" --port "$WPORT3" > "$OUT.m18wh" 2>&1 &
218 W3PID=$!
219 defer_kill "$W3PID"
220 wait_for "$OUT.m18wh" "serving" 10 || {
221 echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; }
222 217
223 # Content for each session, planted by a CLI client and left behind: the 218 # The sessions FIRST: a tile is a live session on a listed daemon, so
224 # tiles are passive 0x0 wall tiles and can never type anything themselves. 219 # there is nothing for the hub to show until these exist. Their content is
220 # planted here too and left behind — the tiles are passive 0x0 wall tiles
221 # and can never type anything themselves.
225 pipe_mux "$OUT.m18wa" "$OUT.m18wa.err" timeout 40 "$MUX" --sock "$SOCK22" --session a 222 pipe_mux "$OUT.m18wa" "$OUT.m18wa.err" timeout 40 "$MUX" --sock "$SOCK22" --session a
226 pipe_send 'printf "wall-a-%%s\\n" pin\n' 223 pipe_send 'printf "wall-a-%%s\\n" pin\n'
227 await_out "$OUT.m18wa" "wall-a-pin" "M18 wall: session a's marker never reached the client" 224 await_out "$OUT.m18wa" "wall-a-pin" "M18 wall: session a's marker never reached the client"
@@ -233,18 +230,44 @@ await_out "$OUT.m18wb" "wall-b-pin" "M18 wall: session b's marker never reached
233 pipe_detach 230 pipe_detach
234 wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b 231 wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b
235 232
236 # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the 233 # Its own state home: `mux web --sock S` RECORDS S in the hosts file it
237 # name the way mux.js does — bytes appended after the fixed 20 — and 234 # then serves, and every other leg's daemons are in the shared one.
238 # attaches at 0x0, the passivity contract: both sessions already exist, so 235 hostroom m18wall
239 # these are joins, and a slot that claimed no size can never move the grid. 236 XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK22" --port "$WPORT3" > "$OUT.m18wh" 2>&1 &
240 for _t in 0 1; do 237 W3PID=$!
241 case "$_t" in 238 defer_kill "$W3PID"
242 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; 239 M18ORIG="http://127.0.0.1:$WPORT3"
243 *) _sn=b; _mine=wall-b-pin; _theirs=wall-a-pin ;; 240 # Three tiles: the daemon's own default session and the two above. `tile 2:`
241 # is the last of them, so waiting on it is waiting for the whole list.
242 wait_for "$OUT.m18wh" "tile 2:" 15 || {
243 echo "e2e FAIL: M18 wall hub never announced its tiles"; cat "$OUT.m18wh"; exit 1; }
244
245 # ONE label, three sessions beside it — which is the milestone's whole
246 # claim, read off the page's own answer.
247 M18WANT="[{\"label\":\"--sock $SOCK22\",\"session\":\"0\"},"
248 M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"a\"},"
249 M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"b\"}]"
250 [ "$(tiles_shape "$M18ORIG")" = "$M18WANT" ] || {
251 echo "e2e FAIL: M18 wall: one host's sessions are not its tiles"
252 echo " got: $(tiles_shape "$M18ORIG")"
253 echo " want: $M18WANT"; exit 1; }
254
255 # The stand-in spells the name the way mux.js does — bytes appended after
256 # the fixed 20 — and attaches at 0x0, the passivity contract: both
257 # sessions already exist, so these are joins, and a slot that claimed no
258 # size can never move the grid.
259 for _sn in a b; do
260 case "$_sn" in
261 a) _mine=wall-a-pin; _theirs=wall-b-pin ;;
262 *) _mine=wall-b-pin; _theirs=wall-a-pin ;;
244 esac 263 esac
264 _t=$(tile_id_of "$M18ORIG" "--sock $SOCK22" "$_sn")
265 [ -n "$_t" ] || {
266 echo "e2e FAIL: M18 wall: session $_sn has no tile; the wall holds:"
267 curl -s "$M18ORIG/tiles"; exit 1; }
245 set +e 268 set +e
246 timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ 269 timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \
247 --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF 270 --out "$OUT.m18ws$_sn" --err "$OUT.m18ws$_sn.err" <<EOF
248 attach 0 0 $_sn 271 attach 0 0 $_sn
249 expectstate up 10000 272 expectstate up 10000
250 expectgrid $_mine 15000 273 expectgrid $_mine 15000
@@ -255,17 +278,17 @@ EOF
255 set -e 278 set -e
256 [ "$RC" -eq 0 ] || { 279 [ "$RC" -eq 0 ] || {
257 echo "e2e FAIL: M18 wall: tile $_t (session $_sn) wsclient exited $RC" 280 echo "e2e FAIL: M18 wall: tile $_t (session $_sn) wsclient exited $RC"
258 cat -v "$OUT.m18ws$_t.err" 2>/dev/null; cat "$OUT.m18wh"; exit 1; } 281 cat -v "$OUT.m18ws$_sn.err" 2>/dev/null; cat "$OUT.m18wh"; exit 1; }
259 282
260 # The replica converges on ITS session's grid... 283 # The replica converges on ITS session's grid...
261 assert_ws_converged "$OUT.m18ws$_t" "$SOCK22" "M18 wall: tile $_t" "$_sn" 284 assert_ws_converged "$OUT.m18ws$_sn" "$SOCK22" "M18 wall: tile $_t" "$_sn"
262 # ...and the other session's content never crossed into it. The 285 # ...and the other session's content never crossed into it. The
263 # convergence check above is a diff against one grid and would pass on 286 # convergence check above is a diff against one grid and would pass on
264 # a hub that fanned every session's frames to every tile only if that 287 # a hub that fanned every session's frames to every tile only if that
265 # grid happened to match; this grep is the direct question. 288 # grid happened to match; this grep is the direct question.
266 if grep -q "$_theirs" "$OUT.m18ws$_t"; then 289 if grep -q "$_theirs" "$OUT.m18ws$_sn"; then
267 echo "e2e FAIL: M18 wall: tile $_t (session $_sn) received the other session's content:" 290 echo "e2e FAIL: M18 wall: tile $_t (session $_sn) received the other session's content:"
268 cat "$OUT.m18ws$_t"; exit 1 291 cat "$OUT.m18ws$_sn"; exit 1
269 fi 292 fi
270 done 293 done
271 294
@@ -274,7 +297,7 @@ wait_pid_gone "$W3PID" "M18 wall: hub killed by tracked pid"
274 W3PID="" 297 W3PID=""
275 assert_stopped "$SOCK22" "$D19PID" "M18 wall" "$OUT.m18wstop" 298 assert_stopped "$SOCK22" "$D19PID" "M18 wall" "$OUT.m18wstop"
276 D19PID="" 299 D19PID=""
277 ok "the wall shows one host twice: two tiles, two sessions, one socket" 300 ok "the wall shows one daemon's sessions as its tiles: three tiles, one socket, no crosstalk"
278 301
279 # --- M18 over QUIC: two dials are two sessions, and a name means one ----- 302 # --- M18 over QUIC: two dials are two sessions, and a name means one -----
280 # --- session whichever transport carried the attach ---------------------- 303 # --- session whichever transport carried the attach ----------------------
test/e2e_06_web.sh
Old New
@@ -20,9 +20,9 @@ defer_sock "$SOCK20"
20 WPORT=$(( 41000 + ($$ % 4000) )) 20 WPORT=$(( 41000 + ($$ % 4000) ))
21 WPORT2=$(( 46000 + ($$ % 4000) )) 21 WPORT2=$(( 46000 + ($$ % 4000) ))
22 # The hosts-wall leg: TWO daemons — a wall of one host is blind to host 22 # The hosts-wall leg: TWO daemons — a wall of one host is blind to host
23 # order and to "only its own list may drop a tile" — a hub restarted once, 23 # order and to "only its own list may drop a tile" — one hub, and a STATE
24 # and a STATE HOME of its own on the 61000 band, the last 5000-spaced one 24 # HOME of its own on the 61000 band, the last 5000-spaced one that fits
25 # that fits under 65535 (the QUIC block's 56000 being the one before). The 25 # under 65535 (the QUIC block's 56000 being the one before). The
26 # state home is the load-bearing part: this leg reads the HOSTS file back 26 # state home is the load-bearing part: this leg reads the HOSTS file back
27 # as an artifact, and $XDG_STATE_HOME above is shared with every other 27 # as an artifact, and $XDG_STATE_HOME above is shared with every other
28 # scenario — every `mux` attach records its daemon there, so a file read 28 # scenario — every `mux` attach records its daemon there, so a file read
@@ -267,6 +267,33 @@ pipe_send 'printf "host-%%s\\n" mk\n'
267 wait_grid "$SOCK25" "host-mk" "host wall: session b exists" b 267 wait_grid "$SOCK25" "host-mk" "host wall: session b exists" b
268 pipe_detach "host wall: the session-b maker" 268 pipe_detach "host wall: the session-b maker"
269 269
270 # An EMPTY file is an empty wall, not a refusal — and specifically not the
271 # local socket: nothing asked for a daemon, and a read never starts one.
272 # Run before the argv hub so the file this leg then builds is built from
273 # nothing, and the one stderr line is asserted where it is the only line.
274 mkdir -p "$DWSTATE/mux"
275 : > "$DWSTATE/mux/hosts"
276 XDG_STATE_HOME="$DWSTATE" "$MUX" web --port "$WPORT4" > "$OUT.dwempty" 2>&1 &
277 W5PID=$!
278 defer_kill "$W5PID"
279 wait_for "$OUT.dwempty" "serving" 10 || {
280 echo "e2e FAIL: host wall: the empty-file hub never served"; cat "$OUT.dwempty"; exit 1; }
281 grep -q "no hosts" "$OUT.dwempty" || {
282 echo "e2e FAIL: host wall: an empty wall said nothing about being empty:"
283 cat "$OUT.dwempty"; exit 1; }
284 [ "$(curl -s "http://127.0.0.1:$WPORT4/tiles")" = "[]" ] || {
285 echo "e2e FAIL: host wall: the empty-file hub served tiles:"
286 curl -s "http://127.0.0.1:$WPORT4/tiles"; exit 1; }
287 # ...and it added nothing. A hub that fell back to the local socket would
288 # have written a line here, and every later assertion in this leg would be
289 # reading a file with a daemon in it that nobody asked for.
290 [ ! -s "$DWSTATE/mux/hosts" ] || {
291 echo "e2e FAIL: host wall: the empty-file hub wrote a host line:"
292 cat "$DWSTATE/mux/hosts"; exit 1; }
293 softkill "$W5PID" || true
294 wait_pid_gone "$W5PID" "host wall: empty-file hub killed by tracked pid"
295 W5PID=""
296
270 # Argv is RECORDED, not a view of its own: `mux web "--sock A" "--sock B"` 297 # Argv is RECORDED, not a view of its own: `mux web "--sock A" "--sock B"`
271 # is `mux hosts add` twice and then a hub on the file. 298 # is `mux hosts add` twice and then a hub on the file.
272 XDG_STATE_HOME="$DWSTATE" "$MUX" web "--sock $SOCK25" "--sock $SOCK76" --port "$WPORT4" > "$OUT.dwh" 2>&1 & 299 XDG_STATE_HOME="$DWSTATE" "$MUX" web "--sock $SOCK25" "--sock $SOCK76" --port "$WPORT4" > "$OUT.dwh" 2>&1 &
@@ -301,15 +328,8 @@ for _t in 0 1 2; do
301 done 328 done
302 # One string, not three greps: which tile sits where is the claim, and 329 # One string, not three greps: which tile sits where is the claim, and
303 # separate greps would pass on a wall holding them in any order at all. 330 # separate greps would pass on a wall holding them in any order at all.
304 dw_shape() { curl -s "$DWORIG/tiles" | sed 's/"id":[0-9]*,//g'; } 331 dw_shape() { tiles_shape "$DWORIG"; }
305 dw_id_of() { # LABEL SESSION -> the hub's id for that tile, or empty 332 dw_id_of() { tile_id_of "$DWORIG" "$1" "$2"; }
306 # One line per tile object, then the id off the line that carries BOTH
307 # this label and this session: a label alone names two tiles whenever a
308 # daemon has two sessions, which is the case this leg is built on.
309 curl -s "$DWORIG/tiles" | tr '{' '\n' |
310 grep -F "\"label\":\"$1\",\"session\":\"$2\"}" |
311 sed -n 's/^"id":\([0-9][0-9]*\),.*/\1/p' | head -1
312 }
313 DWWANT="[{\"label\":\"--sock $SOCK25\",\"session\":\"0\"}," 333 DWWANT="[{\"label\":\"--sock $SOCK25\",\"session\":\"0\"},"
314 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"}," 334 DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"},"
315 DWWANT="$DWWANT{\"label\":\"--sock $SOCK76\",\"session\":\"0\"}]" 335 DWWANT="$DWWANT{\"label\":\"--sock $SOCK76\",\"session\":\"0\"}]"
test/e2e_lib.sh
Old New
@@ -867,6 +867,21 @@ assert_ws_converged() {
867 fi 867 fi
868 } 868 }
869 869
870 # tile_id_of ORIGIN LABEL SESSION — the hub's id for one tile of `GET
871 # /tiles`, or empty. Ids are BIRTH order and one poller thread per host
872 # races the others, so no leg may predict them; the label AND the session
873 # together are what names a tile, because a daemon with two sessions wears
874 # one label twice.
875 tile_id_of() {
876 curl -s "$1/tiles" | tr '{' '\n' |
877 grep -F "\"label\":\"$2\",\"session\":\"$3\"}" |
878 sed -n 's/^"id":\([0-9][0-9]*\),.*/\1/p' | head -1
879 }
880
881 # tiles_shape ORIGIN — `GET /tiles` with the ids struck out: the ORDER and
882 # the labels are a leg's to assert, the numbering is not.
883 tiles_shape() { curl -s "$1/tiles" | sed 's/"id":[0-9]*,//g'; }
884
870 # Scenario checkpoints. The suite's final line asserts the COUNT of these 885 # Scenario checkpoints. The suite's final line asserts the COUNT of these
871 # against a literal: adding or removing a scenario means updating that 886 # against a literal: adding or removing a scenario means updating that
872 # literal, and the friction is the feature — a scenario that silently 887 # literal, and the friction is the feature — a scenario that silently