a73x

ee301abd

feat: muxweb argv adds tiles to the wall, never overwrites it

a73x   2026-08-20 00:50

Commit message
feat: muxweb argv adds tiles to the wall, never overwrites it

Argv used to be saved with wall.save, replacing the file with whatever
that run was told to show. Defensible while the file meant "the last wall
muxweb was given"; it stopped meaning that the moment attaches started
writing to it, and one `muxweb HOST` would then have silently erased
every tile every `mux` had recorded.

Argv still overrides the VIEW — the run shows the tiles it named and only
those — but each is now recorded and deduped, and nothing is removed.
Forgetting stays explicit: the page's remove button, the wall's `x`,
`mux wall rm`.

The e2e assertion INVERTS rather than moving: it used to fail on "argv
appended to the wall instead of replacing it", and now fails if the
pre-existing line does not survive. Both halves are asserted, because the
"argv reached the file" grep alone passes on an overwrite.

src/webhub_main.zig
Old New
@@ -233,12 +233,23 @@ pub fn main() !u8 {
233 else => return err, 233 else => return err,
234 }; 234 };
235 defer hub.deinit(); 235 defer hub.deinit();
236 // Argv is the override, so it is also what the next run restores. Saved 236 // Argv overrides the VIEW — this run shows the tiles it named, and only
237 // only when there WAS argv: a restore that rewrites what it just read 237 // those — but it no longer overwrites the FILE. That file stopped being
238 // would turn a read failure into a lost wall. 238 // "the last wall muxweb was told to show" when attaches started writing
239 if (parsed.tiles.items.len != 0) wall.save(&hub.wall_state, state_path) catch |err| { 239 // to it: it is the user's attach history now, and one
240 std.debug.print("muxweb: cannot save wall {s}: {s}\n", .{ state_path, @errorName(err) }); 240 // `muxweb HOST` would have silently erased every tile every `mux` had
241 return 2; 241 // recorded. So each argv tile is ADDED (deduped by spelling, wall.zig)
242 // and nothing is removed. Forgetting stays explicit, which is the whole
243 // "remove is detach" doctrine: the page's `×`, the wall's `x`,
244 // `mux wall rm`.
245 //
246 // Written only when there WAS argv, still: a restore that rewrote what
247 // it just read would turn a read failure into a lost wall.
248 if (parsed.tiles.items.len != 0) for (hub.wall_state.targets.items) |spelling| {
249 _ = wall.record(alloc, state_path, spelling) catch |err| {
250 std.debug.print("muxweb: cannot save wall {s}: {s}\n", .{ state_path, @errorName(err) });
251 return 2;
252 };
242 }; 253 };
243 254
244 const addr = std.net.Address.parseIp("127.0.0.1", parsed.port) catch unreachable; 255 const addr = std.net.Address.parseIp("127.0.0.1", parsed.port) catch unreachable;
test/e2e.sh
Old New
@@ -4562,10 +4562,16 @@ curl -s "$DWORIG/tiles" | grep -q "^\[{\"id\":0,\"label\":\"--sock $SOCK25#b\""
4562 echo "e2e FAIL: dyn wall: restart lost the persisted wall; it holds:" 4562 echo "e2e FAIL: dyn wall: restart lost the persisted wall; it holds:"
4563 curl -s "$DWORIG/tiles"; exit 1; } 4563 curl -s "$DWORIG/tiles"; exit 1; }
4564 4564
4565 # Restart WITH argv: the explicit override replaces the file rather than 4565 # Restart WITH argv: the override is the VIEW's, not the file's. This
4566 # appending to it, and is itself saved as the next run's wall. The tile is 4566 # assertion INVERTED with the attach-history phase and did not merely move.
4567 # spelled as ONE quoted argument — the wall file's own spelling, handed 4567 # It used to read "argv appended to the wall instead of replacing it" — a
4568 # back to the binary that wrote it; the grep below is what makes it a pin. 4568 # pin on muxweb overwriting the state file with whatever it was told to
4569 # show. That file is the user's attach history now, written by every `mux`
4570 # attach, so one `muxweb HOST` overwriting it would silently erase the lot.
4571 # Argv tiles are ADDED (deduped by spelling) and nothing is removed;
4572 # forgetting stays explicit — the page's `x`, the wall's `x`, `mux wall rm`.
4573 # The tile is spelled as ONE quoted argument — the wall file's own
4574 # spelling, handed back to the binary that wrote it.
4569 kill "$W4PID" 2>/dev/null || true 4575 kill "$W4PID" 2>/dev/null || true
4570 wait_pid_gone "$W4PID" "dyn wall: restored hub killed by tracked pid" 4576 wait_pid_gone "$W4PID" "dyn wall: restored hub killed by tracked pid"
4571 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" "--sock $SOCK25" --port "$WPORT4" > "$OUT.dwh3" 2>&1 & 4577 XDG_STATE_HOME="$DWSTATE" "$MUXWEB" "--sock $SOCK25" --port "$WPORT4" > "$OUT.dwh3" 2>&1 &
@@ -4573,10 +4579,17 @@ W4PID=$!
4573 wait_for "$OUT.dwh3" "serving" 10 || { 4579 wait_for "$OUT.dwh3" "serving" 10 || {
4574 echo "e2e FAIL: dyn wall: overriding hub never reported serving"; cat "$OUT.dwh3"; exit 1; } 4580 echo "e2e FAIL: dyn wall: overriding hub never reported serving"; cat "$OUT.dwh3"; exit 1; }
4575 grep -qxF -- "--sock $SOCK25" "$DWSTATE/mux/wall" || { 4581 grep -qxF -- "--sock $SOCK25" "$DWSTATE/mux/wall" || {
4576 echo "e2e FAIL: dyn wall: argv did not replace the wall file; it holds:" 4582 echo "e2e FAIL: dyn wall: argv did not reach the wall file; it holds:"
4577 cat "$DWSTATE/mux/wall"; exit 1; } 4583 cat "$DWSTATE/mux/wall"; exit 1; }
4578 [ "$(wc -l < "$DWSTATE/mux/wall")" = "1" ] || { 4584 # ...and the line that was already there SURVIVED it. Both halves, because
4579 echo "e2e FAIL: dyn wall: argv appended to the wall instead of replacing it:" 4585 # either alone is satisfied by the wrong behaviour: the grep above passes
4586 # on an overwrite, and a count alone would not say which lines are which.
4587 grep -qxF -- "--sock $SOCK25#b" "$DWSTATE/mux/wall" || {
4588 echo "e2e FAIL: dyn wall: argv ERASED the wall it was added to — an"
4589 echo " attach history overwritten by one muxweb invocation:"
4590 cat "$DWSTATE/mux/wall"; exit 1; }
4591 [ "$(wc -l < "$DWSTATE/mux/wall")" = "2" ] || {
4592 echo "e2e FAIL: dyn wall: the wall file is not the two lines argv added to:"
4580 cat "$DWSTATE/mux/wall"; exit 1; } 4593 cat "$DWSTATE/mux/wall"; exit 1; }
4581 4594
4582 kill "$W4PID" 2>/dev/null || true 4595 kill "$W4PID" 2>/dev/null || true
@@ -4584,7 +4597,7 @@ wait_pid_gone "$W4PID" "dyn wall: overriding hub killed by tracked pid"
4584 W4PID="" 4597 W4PID=""
4585 assert_stopped "$SOCK25" "$D22PID" "dyn wall" "$OUT.dwstop" 4598 assert_stopped "$SOCK25" "$D22PID" "dyn wall" "$OUT.dwstop"
4586 D22PID="" 4599 D22PID=""
4587 ok "the wall is runtime state: add, remove, reorder, restore, argv overrides" 4600 ok "the wall is runtime state: add, remove, reorder, restore, argv adds"
4588 4601
4589 # ---- the CLI wall: the hub's multiattach, in one terminal -------------- 4602 # ---- the CLI wall: the hub's multiattach, in one terminal --------------
4590 # One daemon, two named sessions, one `mux wall` on a real pty showing 4603 # One daemon, two named sessions, one `mux wall` on a real pty showing