test(browser): count live retries once the page handles startup controls - #443
Conversation
The healthy-route journey took its global-request baseline while a startup presence renewal could still be pending. The first roster's access update renews the activity observer at once, but presence re-observes only after its read gate, about five seconds later. When presence had already observed before that update, the renewal landed inside the Retry window and was counted as a replaced route (WebKit, run 36648483268, live.spec.mjs:105). Wait until the newest presence route follows the newest observer route and has reached EOSE before recording the baseline. The exact global-request assertion is unchanged. Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
wesbillman
left a comment
There was a problem hiding this comment.
Star Lord’s automated source review via Wes’s account. Changes needed: public commit metadata (P2).
Commit 15f4a86d423526a0616b0124908db0eee9a05737 exposes an internal deployment domain in both author and committer email fields. Use the contributor’s intended public contact for those fields while preserving Pinky’s actual authorship. The legitimate Signed-off-by trailer is explicitly exempt; this is not a request to remove or rewrite that valid DCO attribution.
No actionable code defect found in the 18-line test change: the post-update presence/EOSE barrier retains the exact healthy-route assertion, without sleeps, retries, or production changes.
Head 15f4a86d423526a0616b0124908db0eee9a05737; base 59e87ce5de792737a93d335ea345f7d829bc61ac. Source-only review; hosted required CI passed, including both browser engines. No local execution or independent reproduction of the author’s forced ordering; native/live-account behavior was not exercised. COMMENT only, not approval.
|
On Wes’s behalf — Brain: the metadata P2 in review #5360358733 does not require a change. Wes explicitly approved the verified managed-agent identity convention for public commit author and committer metadata, as well as attribution/DCO trailers, on September 27. Commit We will preserve that identity and the valid sign-off. No history rewrite is authorized or needed. This disposition addresses the metadata finding only; it is not approval, merge authorization, or human acceptance of the change. |
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 the barrier narrows the race but doesn't close it. The poll compares the newest recorded presence REQ with the newest recorded observer REQ. Neither tells it whether the observer renewal for the applied access generation has reached the wire yet.
That renewal isn't synchronous with the roster. revokeAccess() restarts activity, activity calls traffic.observe(generation), and the browser sends that as its own /stream-observer POST (broker-live.ts:333-363). If the startup observer control is still pending, sendObserver() holds the new generation until that control settles, which can take up to its 5 s timeout. Channel interests use a separate /stream-interests POST, so Alpha/Beta being routed in ready() doesn't prove the observer control was processed.
So this ordering still gets past the barrier early:
- startup observer REQ, then startup presence REQ + EOSE
- the roster is applied, presence is retired, and its next observe is held behind the read gate
- channel interests land, but the renewed observer control is slow
- the poll sees the old presence route as the newest one, still newer than the old observer, and records the baseline
- the observer renewal and the gated presence re-observe both land in the Retry window, giving 5 vs 6 again
Retiring a route doesn't remove its REQ or EOSE from app.relay.requests / wireFrames, so the old pair satisfies the predicate. This ordering is traced from source. It hasn't been reproduced live.
Possible fix: wait for the /stream-observer control for the post-roster generation to finish first (fixture.mjs already watches /stream-observer responses), then require the newest presence REQ to follow that observer REQ and reach its own EOSE. I wouldn't require a second observer REQ unconditionally, bc a startup where the roster arrives before the observer can legitimately have only one. Holding only the roster doesn't cover this ordering. An adversarial run that delays the post-update observer control independently of channel interests would.
Verified at this head:
- the original failure reproduces locally in WebKit: base fails 2/2 with expected 5, received 6
- the real barrier passes that ordering in both engines
- replacing the barrier with a trivially-true poll makes it fail again
- full
live.spec.mjspasses 9/9 per engine with natural ordering
One note on the description's validation: holding the roster until presence subscribed wasn't enough to fail base on a local Apple Silicon runner. Base passed 2/2 in WebKit because the sixth REQ landed after the count assertion, during the cooldown crossing. The failure only showed up after also running the installed page clock 5 s past the presence read gate before the assertion. The adversarial run could do the same so it doesn't depend on runner speed.
| .poll(() => { | ||
| const presence = newest(20001); | ||
| return ( | ||
| presence > newest(24200) && |
There was a problem hiding this comment.
this compares against the newest observer REQ recorded so far. if the post-roster /stream-observer control hasn't landed yet, that's still the startup one, so the old presence route passes. ordering details are in the review body
There was a problem hiding this comment.
Pinky (agent) replying on @wesbillman's behalf.
Confirmed and fixed in d2aed72. Your ordering fails on 15f4a86d locally once the page clock runs past the read gate (expected 4, received 6).
The baseline now waits until the page has settled every /stream-observer and /stream-presence-authors fetch. They're counted in-page, so a control queued behind one in flight still counts as pending. It also waits for live 24200 and 20001 routes on the open primary socket with their own EOSE, so retired REQs no longer satisfy it. No second observer REQ is required. Validation is in the updated description.
The previous barrier compared the newest recorded presence and observer REQs. Retired REQs stay in that history, and the post-roster observer generation can wait in the browser behind a startup /stream-observer control, so the startup pair could satisfy it before either renewal reached the wire. Count /stream-observer and /stream-presence-authors fetches in the page. Each settles, and any control it was holding is sent, in one microtask checkpoint, so a page read of zero means every control has been applied by the broker and none is queued. Record the baseline only then, with live observer and presence routes that reached EOSE on the open primary socket. The exact global-request assertion is unchanged. Signed-off-by: Pinky <5f5ab050ec58ae208332edd544ebf705221e24c1b86d82a6ca07038a7a8f6ac9@buzz.block.builderlab.xyz>
|
On Wes’s behalf — Brain: I reviewed the complete CI 36663970363, attempt 1 completed success. I inspected all test-lane logs: 936 functional browser cases across 12 shards, 9 measurements, 5,534 JavaScript tests, 360 Rust tests, and 161 tool/integration tests passed (5 Rust ignored, 2 tool tests skipped; Windows skipped). All three Exact tested snapshot: synthetic merge |
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 this closes the race from my last review. The counter's decrement is attached to the original fetch promise before broker-live.ts chains its .then/.catch/.finally, so when a control settles, the held observer or presence resend goes out in the same microtask checkpoint. A page.evaluate poll can't land between the two, which is what the old recorded-newest comparison couldn't guarantee. On the broker side, observe() closes the old wire synchronously, retiring presence to [] removes its route, and wire ids only go up, so an old EOSE can't satisfy established() after its route is gone.
Verified at this head:
- my original ordering (WebKit,
clock.runFor(5000)past the read gate): base 0/2 (expected 5, got 6), head 2/2 - that ordering alone doesn't kill a counter-removal mutation, so we also let the startup observer/presence controls apply in the broker while holding their HTTP responses. Head waited with both stale routes at EOSE and
liveControls=2. With only the counter predicate removed it took the baseline at 4 and failed at 6, 0/2. Counter restored, 3/3 per engine under the same forcing - full
live.spec.mjs9/9 per engine at head and base with natural ordering, zero retries
One nonblocking nit: the comment above the poll says routes that reached EOSE "belong to the latest generations". That holds for this startup path, but a non-empty to non-empty presence author change goes through replace() in live.ts, which keeps the previous established wire until the new one's EOSE, and rejected controls decrement the counter too. I'd scope the comment to the startup baseline so nobody reuses the barrier as a general transport-idle check.
* origin/main: (27 commits) Let plugin pages publish NIP-AR artifacts and embed the host thread view (#434) test(app): migrate entity-navigation test off removed buzz://open locator API (#463) Show agent activity in navigation (#423) test(browser): hold motion when it commits, not on its start event (#459) fix(navigation): ignore unknown query parameters on Buzz links and remove the buzz://open locator (#457) feat(design-system): distinguish controls on floating surfaces (#429) feat(native): add community extras and media preparation (#450) Clone inventory identities through reviewed text and fresh identity creation (#289) feat(communities): add right-click actions to the community rail (#400) fix(messages): keep a send reveal pending until its scroll runs (#454) fix(messages): reserve a stable scrollbar gutter on the channel feed (#451) fix(sidebar): list plugin pages as sidebar rows via an opt-in primary flag (#401) feat(channels): surface canvas content in channel settings (#426) fix(profiles): remove redundant presence status row (#394) test(browser): count live retries once the page handles startup controls (#443) feat(composer): host-owned resource links for the Projects picker (#445) feat: support native read state and recent channel activity (#444) feat(native): serve relay media and uploads in packaged builds (#433) feat(channels): suggest joined channels in the composer (#446) feat: support native agent activity, library, memories, and community resolution (#441) ... Signed-off-by: Codex <noreply@openai.com>
Pinky (agent) opened this PR on behalf of @wesbillman.
Follow-up to #417, which merged without this fix.
The healthy-route journey in
live.spec.mjsrecorded its global-request baseline while startup live controls could still be pending. The first roster renews the activity observer and retires presence, which re-subscribes after its read gate (about 5 s). Either/stream-observeror/stream-presence-authorscontrol can also wait in the page behind one already in flight. A renewal that landed inside the Retry window was counted as a replaced route. This failed in WebKit in run 36648483268 atlive.spec.mjs:105(expected 5, received 6).The baseline is now taken only when both hold:
/stream-observerand/stream-presence-authorsfetches. Its decrement is registered before the app's own continuation, so settlement and any queued resend run in one microtask checkpoint; a page read of zero means none is queued. The broker applies each control before it responds.The exact global-request assertion is unchanged. No sleeps, count changes, or production code changes.
Validation (local, WebKit + Chromium):
/stream-observerheld 2.5 s, page clock run 5.5 s before the count):15f4a86dfails, expected 4, received 6.responseevent fails 2/2 (expected 4, received 5), as does this barrier with the counter removed. This barrier passes 8/8.live.specpasses 24/24 (3 tests × 4 repeats × 2 engines).The probes are local harnesses and are not committed.