test(notifications): close per-room notification-level audit gaps (LC-90) #178

Merged
nrupard merged 2 commits from feat/lc-90-per-room-notify-audit into main 2026-05-22 17:31:40 +02:00
Owner

What

LC-90 was an audit of the per-room notification level (mute_mode: none / except_mentions / all), not a green-field feature. The schema, write path, render path, and dispatch gating already ship. This PR records the audit result and adds the integration-test tuples the acceptance criteria asked for.

Audit result: works end-to-end, no production change needed

  • UI. The all/mentions/none dropdown (room/notify_dropdown.html) is included unconditionally in the shared partials/room_header.html, so it renders for public, private, and enclave-scoped rooms. DMs are excluded by design (separate binary mute in dm_header.html).
  • Persist + render. POST /room/:id/notify-prefs upserts via set_room_mute_mode, rejects DMs (400), and returns a swapped #lc-room-header fragment (no full reload). The button label reflects the persisted mode.
  • Web Push. push::dispatch gates on room_mute_mode: a mention is dropped only for all, delivered for except_mentions / none.
  • Email digest. load_missed_mentions / load_missed_dms filter mute_mode <> 'all', so except_mentions keeps @mentions in the digest while all drops them.

Semantic note (by design, not a gap)

Push and the digest never fire on plain (non-mention) messages at all - only on @mentions, DMs, and reminders. So all vs except_mentions differ only for: (1) @mention push (suppressed only by all), (2) @mention inclusion in the digest (suppressed only by all), and (3) the sidebar unread badge (suppressed by both all and except_mentions). Plain messages always render in-app regardless. This matches the intended "all = mute everything, mentions = only mentions notify, none = default" semantics. No follow-up issue is warranted (AC #5).

New tests

  • routes_room_mute::private_room_page_renders_notify_dropdown - the seeded general room already covers public + enclave-scoped; this guards a standalone private channel rendering the same dropdown.
  • push_dispatch::dispatch_fires_when_room_mute_none - completes the none/except_mentions/all push triplet (the suite covered only the latter two).
  • email_digest_dispatch::except_mentions_keeps_mention_in_digest_set - symmetric to the existing muted-all exclusion test; locks in that except_mentions keeps @mentions in the digest.

Test

just check, just test, just test-saas all green (85 ok / 0 failed each).

Acceptance criteria

  • Toggle reachable from the room header for every room kind (public / private / enclave-scoped); DMs excluded by design.
  • Toggling updates the DB and re-renders the header without a full reload.
  • Web Push and email-digest dispatch both consult room_mute_mode.
  • Integration tests assert each (mode, message-kind) tuple.
  • No real gap found, so no follow-up issue (the audit note explains the all vs except_mentions equivalence for plain messages).

🤖 Generated with Claude Code

## What LC-90 was an audit of the per-room notification level (`mute_mode`: `none` / `except_mentions` / `all`), not a green-field feature. The schema, write path, render path, and dispatch gating already ship. This PR records the audit result and adds the integration-test tuples the acceptance criteria asked for. ## Audit result: works end-to-end, no production change needed - **UI.** The all/mentions/none dropdown (`room/notify_dropdown.html`) is included unconditionally in the shared `partials/room_header.html`, so it renders for public, private, and enclave-scoped rooms. DMs are excluded by design (separate binary mute in `dm_header.html`). - **Persist + render.** `POST /room/:id/notify-prefs` upserts via `set_room_mute_mode`, rejects DMs (400), and returns a swapped `#lc-room-header` fragment (no full reload). The button label reflects the persisted mode. - **Web Push.** `push::dispatch` gates on `room_mute_mode`: a mention is dropped only for `all`, delivered for `except_mentions` / `none`. - **Email digest.** `load_missed_mentions` / `load_missed_dms` filter `mute_mode <> 'all'`, so `except_mentions` keeps @mentions in the digest while `all` drops them. ### Semantic note (by design, not a gap) Push and the digest never fire on plain (non-mention) messages at all - only on `@mentions`, DMs, and reminders. So `all` vs `except_mentions` differ only for: (1) @mention push (suppressed only by `all`), (2) @mention inclusion in the digest (suppressed only by `all`), and (3) the sidebar unread badge (suppressed by both `all` and `except_mentions`). Plain messages always render in-app regardless. This matches the intended "all = mute everything, mentions = only mentions notify, none = default" semantics. No follow-up issue is warranted (AC #5). ## New tests - `routes_room_mute::private_room_page_renders_notify_dropdown` - the seeded general room already covers public + enclave-scoped; this guards a standalone private channel rendering the same dropdown. - `push_dispatch::dispatch_fires_when_room_mute_none` - completes the none/except_mentions/all push triplet (the suite covered only the latter two). - `email_digest_dispatch::except_mentions_keeps_mention_in_digest_set` - symmetric to the existing muted-all exclusion test; locks in that `except_mentions` keeps @mentions in the digest. ## Test `just check`, `just test`, `just test-saas` all green (85 ok / 0 failed each). ## Acceptance criteria - [x] Toggle reachable from the room header for every room kind (public / private / enclave-scoped); DMs excluded by design. - [x] Toggling updates the DB and re-renders the header without a full reload. - [x] Web Push and email-digest dispatch both consult `room_mute_mode`. - [x] Integration tests assert each (mode, message-kind) tuple. - [x] No real gap found, so no follow-up issue (the audit note explains the `all` vs `except_mentions` equivalence for plain messages). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(notifications): close per-room notification-level audit gaps (LC-90)
All checks were successful
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (push) Successful in 9s
check-secrets / Nosey parker (pull_request) Successful in 2s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 1m20s
b4c8315cae
LC-90 was an audit of the existing per-room notification level (mute_mode: none / except_mentions / all), not a green-field feature. The full surface already ships and works end-to-end; this commit adds the few integration-test tuples the acceptance criteria asked for but were not yet covered.

Audit findings (all already correct, no production change needed): the all/mentions/none dropdown lives in the shared room_header.html and renders unconditionally for public, private, and enclave-scoped rooms (DMs use a separate binary mute by design). POST /room/:id/notify-prefs upserts and returns a swapped header fragment (no full reload). Web Push gates on room_mute_mode (a mention is dropped only for 'all', delivered for 'except_mentions'/'none'); the email digest SQL excludes only 'all'. Push and digest never fire on plain non-mention messages at all, so 'all' vs 'except_mentions' differ only for @mentions (push + digest) and the sidebar unread badge (suppressed by both 'all' and 'except_mentions') - this is by design, not a gap.

New tests:
- routes_room_mute::private_room_page_renders_notify_dropdown - the seeded general room already covers public + enclave-scoped; this guards a standalone private channel rendering the same dropdown (AC: reachable for every room kind).
- push_dispatch::dispatch_fires_when_room_mute_none - completes the none/except_mentions/all push triplet (the existing suite covered only the latter two).
- email_digest_dispatch::except_mentions_keeps_mention_in_digest_set - symmetric to the existing muted-all exclusion test; locks in that 'except_mentions' keeps @mentions in the digest set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
test(notifications): assert private-room dropdown for a non-admin member (LC-90)
All checks were successful
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / TruffleHog (push) Successful in 6s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (push) Successful in 7s
check-secrets / Nosey parker (pull_request) Successful in 7s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 3m50s
0de703fc01
Review follow-up: the original private_room_page_renders_notify_dropdown rendered the page as the admin viewer, which only proved an admin sees the dropdown. Strengthen it to view through a non-admin member's own session, since the acceptance criterion is "reachable for every room kind" for users with access, not just admins.

A non-admin reaches a private room through enclave membership plus an explicit room_members row, so the room is now created inside the seeded General enclave (peer is a member via backfill) rather than standalone; an enclave-less private room 403s for a non-admin even with a room_members row. TestApp now also carries the auth pool so the test can mint the peer's session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-90-per-room-notify-audit 2026-05-22 17:31:40 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!178
No description provided.