4e7623ee
docs: verify.js is where a page rule gets pinned, not a browser
a73x 2026-08-26 12:30
Commit message
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -6909,5 +6909,12 @@ nothing in `test/` can see it. `wsclient` stands in for the browser on the wire | |||
| 6909 | but attaches `0 0` and never executes `mux.js`, so every rule that lives in the | 6909 | but attaches `0 0` and never executes `mux.js`, so every rule that lives in the |
| 6910 | page — this one, the `up` re-attach, the badge vocabulary — is invisible to the | 6910 | page — this one, the `up` re-attach, the badge vocabulary — is invisible to the |
| 6911 | e2e suite by construction. Faking a leg with `wsclient` would assert the | 6911 | e2e suite by construction. Faking a leg with `wsclient` would assert the |
| 6912 | fixture's behavior, not the page's. A real-browser pass is the gate for mux.js | 6912 | fixture's behavior, not the page's. |
| 6913 | behavior, and this entry is the reason to keep paying for one. | 6913 | |
| 6914 | The gate for those rules is `web/verify.js`, which runs `mux.js` under | ||
| 6915 | `zig build test` (gated on `node`): it constructs a real `Tile`, feeds it wire | ||
| 6916 | bytes, and calls `sendAttach`. State rules belong there — `verifyStatusShell` | ||
| 6917 | pins this latch, the refusal that must keep re-attaching, `setStatus`'s | ||
| 6918 | precedence and both badge words, and each check was watched failing against the | ||
| 6919 | mutation that removes its rule. A real-browser pass stays the gate for what only | ||
| 6920 | a browser has: rendering, input, and the platform APIs the shell talks to. | ||
src/webhub.zig
| Old | New | ||
|---|---|---|---|
| @@ -698,10 +698,6 @@ fn drainBrowser( | |||
| 698 | 698 | ||
| 699 | /// False means the tile is over. Close is idempotent, so a failed | 699 | /// False means the tile is over. Close is idempotent, so a failed |
| 700 | /// re-dial leaves the caller's deferred close with nothing to do. | 700 | /// re-dial leaves the caller's deferred close with nothing to do. |
| 701 | /// | ||
| 702 | /// Takes the Restore because its per-dial half is reset HERE rather than | ||
| 703 | /// at the three call sites: one of them forgot, and a forgotten reset is | ||
| 704 | /// silent until a torn transport turns the next refusal into an ending. | ||
| 705 | fn redial( | 701 | fn redial( |
| 706 | alloc: std.mem.Allocator, | 702 | alloc: std.mem.Allocator, |
| 707 | transport: *client.Transport, | 703 | transport: *client.Transport, |
| @@ -711,6 +707,9 @@ fn redial( | |||
| 711 | live: *Liveness, | 707 | live: *Liveness, |
| 712 | restore: *Restore, | 708 | restore: *Restore, |
| 713 | ) bool { | 709 | ) bool { |
| 710 | // The per-dial reset belongs here, not at the three call sites: one of | ||
| 711 | // them forgot, and a forgotten reset stays silent until a torn | ||
| 712 | // transport turns the next refusal into an ending. | ||
| 714 | restore.onRedial(); | 713 | restore.onRedial(); |
| 715 | ws.writeMessage(controlMessage(.reconnecting), .binary) catch return false; | 714 | ws.writeMessage(controlMessage(.reconnecting), .binary) catch return false; |
| 716 | transport.close(); | 715 | transport.close(); |
| @@ -720,37 +719,33 @@ fn redial( | |||
| 720 | } | 719 | } |
| 721 | 720 | ||
| 722 | /// The restore rule's state, one value rather than three locals so that | 721 | /// The restore rule's state, one value rather than three locals so that |
| 723 | /// `redial` can own the per-dial reset. A dial that skips it inverts the | 722 | /// `redial` can own the per-dial reset. |
| 724 | /// next exit_status: `saw_grid` carried over reads the REFUSAL that | ||
| 725 | /// follows as "the shell exiting", latches `ended`, and disables the | ||
| 726 | /// heal for the rest of the pump's life — silently, since a tear is the | ||
| 727 | /// only way to reach it. | ||
| 728 | /// | ||
| 729 | /// Only `saw_grid` is per-DIAL — it is the browser's own discriminator | ||
| 730 | /// (mux.js, the exit_status case): before any grid an exit_status is the | ||
| 731 | /// daemon refusing the attach, after one it is the shell exiting. The | ||
| 732 | /// other two outlive the connection ON PURPOSE, because every refusal | ||
| 733 | /// closes it: `serviceObserver` answers an unseated attach with | ||
| 734 | /// exit_status and then `dropObserver`, so a flag cleared on `.closed` | ||
| 735 | /// bounds nothing at all. | ||
| 736 | /// | ||
| 737 | /// `ended` is wallview's "exited stays exited" (its pump ENDS on an | ||
| 738 | /// exit_status rather than redialing). Without it a user typing `exit` | ||
| 739 | /// gets a new shell: the daemon reaps the session and closes, the hub | ||
| 740 | /// redials, mux.js re-attaches on `up`, the attach is refused because | ||
| 741 | /// the session is gone — and that is indistinguishable from the restore | ||
| 742 | /// case unless the pump remembers it already watched this session die. | ||
| 743 | /// | ||
| 744 | /// `birth_tried` bounds a daemon that refuses the birth too (a full | ||
| 745 | /// table, a name it will not make) to ONE attempt per healthy period: it | ||
| 746 | /// is cleared by a grid arriving, not by a re-dial, so a second daemon | ||
| 747 | /// restart still heals while a refuse/redial spin cannot fork a process | ||
| 748 | /// per turn. | ||
| 749 | const Restore = struct { | 723 | const Restore = struct { |
| 724 | // The only per-DIAL flag, and the browser's own discriminator (mux.js, | ||
| 725 | // the exit_status case): before any grid an exit_status is the daemon | ||
| 726 | // refusing the attach, after one it is the shell exiting. A dial that | ||
| 727 | // keeps it inverts the next exit_status — the refusal after a tear | ||
| 728 | // reads as the shell exiting, latches `ended`, and disables the heal | ||
| 729 | // for the rest of the pump's life, silently, because a tear is the | ||
| 730 | // only way to reach it. | ||
| 750 | saw_grid: bool = false, | 731 | saw_grid: bool = false, |
| 732 | // Bounds a daemon that refuses the birth too (a full table, a name it | ||
| 733 | // will not make) to ONE attempt per healthy period: cleared by a grid | ||
| 734 | // arriving, not by a re-dial, so a second daemon restart still heals | ||
| 735 | // while a refuse/redial spin cannot fork a process per turn. | ||
| 751 | birth_tried: bool = false, | 736 | birth_tried: bool = false, |
| 737 | // wallview's "exited stays exited" (its pump ENDS on an exit_status | ||
| 738 | // rather than redialing). Without it a user typing `exit` gets a new | ||
| 739 | // shell: the daemon reaps the session and closes, the hub redials, | ||
| 740 | // mux.js re-attaches on `up`, the attach is refused because the | ||
| 741 | // session is gone — indistinguishable from the restore case unless | ||
| 742 | // the pump remembers it already watched this session die. | ||
| 752 | ended: bool = false, | 743 | ended: bool = false, |
| 753 | 744 | ||
| 745 | // Only `saw_grid` resets. The other two outlive the connection ON | ||
| 746 | // PURPOSE, because every refusal closes it: `serviceObserver` answers | ||
| 747 | // an unseated attach with exit_status and then `dropObserver`, so a | ||
| 748 | // flag cleared on the close bounds nothing at all. | ||
| 754 | fn onRedial(self: *Restore) void { | 749 | fn onRedial(self: *Restore) void { |
| 755 | self.saw_grid = false; | 750 | self.saw_grid = false; |
| 756 | } | 751 | } |
| @@ -763,9 +758,9 @@ const Restore = struct { | |||
| 763 | if (t == .exit_status and self.saw_grid) self.ended = true; | 758 | if (t == .exit_status and self.saw_grid) self.ended = true; |
| 764 | } | 759 | } |
| 765 | 760 | ||
| 766 | /// Read AFTER onFrame: an exit_status this pump has not answered and | 761 | // Read AFTER onFrame: an exit_status this pump has not answered and |
| 767 | /// cannot read as an ending. Whether the target may be recreated at | 762 | // cannot read as an ending. Whether the target may be recreated at all |
| 768 | /// all is the caller's half (`client.hydratedCreates`). | 763 | // is the caller's half (`client.hydratedCreates`). |
| 769 | fn wantsBirth(self: Restore, t: proto.MsgType) bool { | 764 | fn wantsBirth(self: Restore, t: proto.MsgType) bool { |
| 770 | return t == .exit_status and !self.saw_grid and !self.ended and !self.birth_tried; | 765 | return t == .exit_status and !self.saw_grid and !self.ended and !self.birth_tried; |
| 771 | } | 766 | } |