fix(ui): close the review findings from #510's notification_status fix - #550
Conversation
Independent reviewReviewed at The M1 pin genuinely pins, needle by needleSimulated statically against the checked-out source:
Each deletion is caught twice. The second mutation's stated consequence also checks out: m8 is aria-clean and layout-clean, and two design calls are vindicatedThe repo's known trap — a badge's Keeping the blocked state off Layout is safe — Both exclusions upheld. Dioxus traps checked clean: MINOR-1 — the
|
Mutation testing at #542's head showed both statements of the re-arm could be deleted with the whole suite green. Each shipped a silent regression: without the ENABLE_PROMPT_SENT reset an unanswered prompt never re-arms (#510's 'no way to retry'), and without the ENABLE_PROMPT_REARMS increment the counter stays at 0, so the cap never binds and the shell's bar returns on every message sent. the_automatic_rearm_is_bounded does not cover the second: it feeds the predicate a caller-supplied count, so it passes with the increment gone. The statics are process-global and native tests run in parallel threads, so a behavioural test is not available; a source pin has no such problem. Also raise the unrecognised-status log from debug! to warn!. release_max_level_info compiles debug! out, and that line fires exactly when the cross-repo status contract has drifted — the failure that reverts #510 wholesale.
The per-room modes only decide WHEN River wants to notify. If the browser is refusing, all of them are inert, and the only place that said so was inside the modal — so a user with a blocked permission saw an ordinary bell reading "Notifications: All messages" and learned nothing unless they went looking. For an issue titled "fails silently", that was the last place the silence lived. Adds a decorative dot plus an aria-label explaining it. Deliberately not shown for a Muted room (the user asked for no notifications) or for Unsupported (nothing the user does could ever clear it). Also makes the unrecognised-status assertion in the shell spec non-vacuous: it asserted text that was already present from the preceding status and passed on its first poll, so it would have passed equally had the junk clobbered the stored value. It now waits out a window for the non-event, and then posts a recognised status to prove the listener survived — a parse that threw would otherwise look identical to correctly ignoring the junk.
MINOR-2: ring-panel generated no CSS. --color-panel is declared on :root, outside the @theme block, and Tailwind v4 only derives colour utilities from --color-* registered in @theme; there was no @Utility ring-* either, so ring-1 fell back to currentColor and the separator ring rendered grey, blue on hover. Verified by grepping the built stylesheet: no .ring-panel rule existed, and .ring-1 resolves var(--tw-ring-color,currentcolor). Adds the hand-written @Utility alongside the ones every other :root colour already needed, and confirmed .ring-panel{--tw-ring-color:var(--color-panel)} now generates. (ring-[color:var(--color-panel)] was tried first and also generated nothing.) MINOR-1: the warn! from the previous commit was itself unpinned — reverting that one token left the whole suite green, the same silent-failure shape this PR exists to close. Pinned two ways: the framed Playwright test now waits for the listener's own console line, which also replaces the fixed 500ms sleep with a positive signal that the junk was handled (a slow clobber slipped past the sleep as a false pass rather than a flake), and a source pin gives the same guarantee in the fast native job without depending on Dioxus spelling the level 'WARN' in the message text.
Review minors addressed, and rebasedMINOR-1 — the
Worth recording: Dioxus writes every level to Both instruments verified against the mutation: reverting to MINOR-2 — One correction to the suggested fix: MINOR-3 — filed as freenet/freenet-core#5043, not changed here. I verified all three paths in Rebased onto current Re-verified on the rebased tree, not on the pre-rebase one: 792 native tests, 50 Playwright across all five projects, and 207 in the layout-sensitive specs — Not merging and not arming auto-merge. CI is running on [AI-assisted - Claude] |
Follow-up to #542 (issue #510), which had already merged as
f262d79dby thetime independent review came back. These are that review's findings, plus the
one item it left to my judgement.
Problem
M1 — the "reset the flag" half of #510 had zero coverage. Both statements of
the re-arm at
notifications.rscould be deleted with the entire suite green:Each shipped a silent regression. Without the
SENTreset an unanswered promptnever re-arms, which is literally the "no way to retry" the issue is about.
Without the
REARMSincrement the counter stays at 0, sorearms_used < MAXisalways true and the re-arm becomes unbounded — the shell's affordance bar
returns on every message the user sends, the exact nag the cap exists to prevent.
My own review comment on #542 claimed
the_automatic_rearm_is_boundedcoveredthe second mutation. It does not: that test feeds the predicate a
caller-supplied
rearms_used, so it passes with the increment gone. The row wastrue for changing the constant, not for breaking the counter. My disclosed
reasoning ("no native test drives the real atomics — process-global statics vs
parallel test threads") correctly ruled out a behavioural test but stopped
there, missing the cheap remedy this same file already uses four times.
m4 — a vacuous assertion in
notification-shell-status.spec.ts. Theunrecognised-status check asserted text that was already on screen from the
preceding
denied, andtoContainTextpassed on its first poll before the junkcould have been processed. It would have passed just as well had the junk
clobbered the stored status.
m3 —
debug!for an unrecognised status is compiled out in release(
release_max_level_info). That line fires exactly when the cross-repo statuscontract has drifted, which by my own test's rationale "reverts #510 exactly".
m8 — the status was only visible inside the bell modal. A user whose
permission is
Deniedsaw an ordinary bell reading "Notifications: Allmessages" and learned nothing unless they went looking. For an issue titled
"fails silently", that was the last place the silence lived.
Approach
M1: a source pin, which has no parallelism problem. It asserts each
statement individually with its own failure message, and then the whole guarded
block as one string so the two cannot drift apart or escape the bounded
predicate.
m4: the junk check is a non-event, so it now waits out a window rather
than racing — the write path is a single
setTimeout(0), so a clobber renderswell inside it. It then posts a recognised status and waits for that to land,
because a parse that threw and killed the listener would otherwise look
identical to correctly ignoring the junk.
m3:
warn!.m8: I implemented it rather than declaring it out of scope. The bell gets a
small dot plus an
aria-labelexplaining why. Two deliberate exclusions: aMuted room is never marked (the user asked for no notifications, so warning
them they'll get none is noise), and Unsupported is never marked (nothing
the user does could ever clear it — a permanent badge is nagging, not
informing; the modal still explains it). The button's
titlestays the per-roommode alone, so the tooltip keeps naming exactly one thing and the existing
exact-title assertions are untouched; the blocked state rides on
aria-labeland the dot.
Testing
cargo test -p river-ui --bins: 791 pass (4 new). Playwright: 50 pass acrosschromium, firefox, webkit, mobile-chrome and mobile-safari, plus 214 in the
layout-sensitive specs (
responsive-layout,message-layout,room-header-links,room-info-key-selection) since this adds an element tothe conversation header.
Every new assertion was mutation-verified, including the two the reviewer proved
had survived:
ENABLE_PROMPT_SENT.store(false, ...)an_unanswered_prompt_actually_rearms_the_flag_it_boundsENABLE_PROMPT_REARMS.fetch_add(1, ...)matches!delivery_is_blockedalways falsethe_bell_is_marked_only_when_a_wanted_notification_cannot_arrive, plus both browser badge testsUndecided(clobber)an_unrecognised_status_is_ignored_rather_than_guessed, and now the browser junk check atnotification-shell-status.spec.ts:128That last row is the point of the m4 fix: against the clobbering build the
browser assertion fails with
Received string: "The notification prompt closed without an answer...". The version it replaces passed.Closes the review of #542.
[AI-assisted - Claude]
On the record: two residual silent paths, on the shell side (freenet-core, not this PR)
Independent review of #542 surfaced these. I verified both against
crates/core/src/server/path_handlers/assets/shell_bridge.json freenet-coremainrather than relaying them, since they are claims about another repo.showAppNotificationreturns silently when the Notifications API is absent, when
Notification.permission !== 'granted', or whencontractHasConsent()isfalse. None of those paths calls
notifyStatusToIframe.if (notifyAffordanceShown) return;inmaybeOfferNotifications.Net effect, and it is the interesting one: a
grantedrecorded once can keepRiver displaying "Desktop notifications are enabled" indefinitely while every
delivery is dropped — for instance after the user revokes the permission in
site settings, which River cannot observe from an opaque origin.
That is the same staleness class this PR's predecessor fixed on the top-level
path, where River reads the browser's live answer and ignores what it
remembered. Framed, River has no such reading available, so only the shell can
close it: it would need to report the drop rather than returning silently. The
fix therefore belongs in freenet-core, not here. Happy to file it if wanted.