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>
This commit is contained in:
2026-08-18 14:35:23 -05:00
parent b458c1f46f
commit 5fa88baa0a
7 changed files with 719 additions and 2 deletions

View File

@@ -309,6 +309,39 @@ test('a non-function projectRoster is rejected at registration, not at call time
)
})
// ── pageUrlTemplate: the optional fifth member (§6.4) ──────────────────────
//
// Data, not a method, and the only thing core can use to link to a Team page —
// Teams have no core surface, so the module that owns the page has to say where
// it is. Validated hard because the output goes into an email as a link.
test('pageUrlTemplate is optional: a provider without it registers fine', () => {
const api = registries.stage('uo')
assert.doesNotThrow(() => api.registerTeamProvider(ok()))
})
test('a relative template is kept exactly as given', () => {
register('uo', { ...ok(), pageUrlTemplate: '/uo/guilds/{externalId}' })
assert.equal(registries.registeredTeamProvider().pageUrlTemplate, '/uo/guilds/{externalId}')
})
test('an absolute template is refused — a module may not redirect the sites mail', () => {
const api = registries.stage('uo')
for (const bad of [
'https://evil.test/{externalId}',
'//evil.test/x',
'uo/guilds/{externalId}', // not rooted
'/uo/guilds/{externalId}?x=<script>',
42,
]) {
assert.throws(
() => api.registerTeamProvider({ ...ok(), pageUrlTemplate: bad }),
/pageUrlTemplate must be a relative path/,
`expected "${bad}" to be refused`,
)
}
})
test('an unregistered method cannot ride along into the provider core calls', () => {
register('uo', { ...ok(), somethingElse: async () => 'hi' })
assert.equal(registries.registeredTeamProvider().somethingElse, undefined)