Commit Graph

2 Commits

Author SHA1 Message Date
11b4368b57 feat(teams): phase 8 — the notifications bridge, and the gate §7.2 could not check
All checks were successful
PR Checks / client-build (pull_request) Successful in 31s
PR Checks / bot-tests (pull_request) Successful in 33s
PR Checks / server-tests (pull_request) Successful in 10m49s
The same Team event as §6, delivered a third time: push, email, and now a
Discord channel the operator configured. Not a second pipeline — teamNotify.js
already computed the recipient set once, so the bridge is a sink beside the two
that were there.

The design's gate has no data source. §7.2 bridges an event only if "its
visibility is public, or its destination channel is configured for a
members-only Team context". The four team.* streams carry no visibility; forum
threads have no public/members column because a forum is members-only by
construction; and core cannot see a Discord channel's permissions. So §7.2's own
example config names exactly the two events that are never public.

The gate is therefore an attributed operator acknowledgement, in the shape
teams_forum_uploads_ack already uses. It is a precondition — 422, not a quiet
drop at delivery — it is re-asked at delivery as well as at the save, and
changing the channel clears it, because an acknowledgement is about a
destination and cannot survive the destination changing underneath it.

The design's DDL cannot hold its own default row: MariaDB coerces every PRIMARY
KEY column to NOT NULL, so `team_id NULL` — the deployment-wide default every
override overrides — is unrepresentable. Proved on a real MariaDB (error 1048).
Replaced with a surrogate id, a generated team_key AS IFNULL(team_id, 0) in the
unique key, and the foreign key the original had no room for.

One-shot, not queued: "identical to announce and mod-reverse" names two
different reliability models, and a Team notification is the moment it
describes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 20:25:30 -05:00
5fa88baa0a test(teams): the refusals, which is most of what a notification feature is
A notification feature is mostly things that correctly do NOT happen, and each of
these is invisible until it goes wrong in production: a departed member and a
revoked guest are not recipients; a mute subtracts per Team and leaves the user's
other Teams alone; the author of a post never receives the notification about it;
forums switched off silences the forum streams including the digest; a Team's
first roster wakes nobody; a failed send does not stamp `last_digest_at`.

Two real defects came out of writing them.

`Number(null)` is 0 and 0 is an integer, so a null in a caller's id list survived
`filter(Number.isInteger)` and rode into an IN clause as user id 0. No row has id
0, so it was harmless — which is exactly why it would never have been noticed.
Fixed in all three places that filter ids.

`recipientIds: db.recipientIds` in the model captured the function OBJECT at
require time, so the layer below could never be substituted. That is not only
untestable; it means the model was not really the seam it claimed to be. Wrapped
so `db.x` resolves at call time.

The registries catalog assertion is now an exact five-element list, so a
shard-content stream creeping back into core's registration fails here rather
than shipping.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 14:35:23 -05:00