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:
@@ -13,6 +13,7 @@ const registries = require('../src/modules/registries')
|
||||
const teamsDb = require('../src/model/teams/teams.db')
|
||||
const moderation = require('../src/model/teams/teamModeration.model')
|
||||
const activity = require('../src/model/teams/teamActivity.model')
|
||||
const teamNotify = require('../src/utils/teamNotify')
|
||||
const settings = require('../src/model/settings/settings.model')
|
||||
const teamSync = require('../src/model/teams/teamSync.model')
|
||||
|
||||
@@ -203,6 +204,13 @@ function stubDb() {
|
||||
store.activity.push(item)
|
||||
return true
|
||||
})
|
||||
|
||||
// The push half of the same run (TEAMS.md §6.2, phase 6). Captured the same way
|
||||
// and for the same reason: the fan-out is its own unit, and what belongs here is
|
||||
// WHEN the reconciler decides to fire one.
|
||||
store.tickles = []
|
||||
patch(teamNotify, 'memberJoined', async () => { store.tickles.push('member.joined') })
|
||||
patch(teamNotify, 'leadershipChanged', async () => { store.tickles.push('leadership.changed') })
|
||||
}
|
||||
|
||||
// A provider whose answers the test controls. Defaults are authoritative and
|
||||
@@ -851,6 +859,39 @@ test('the FIRST roster emits nothing — an import is not 155 people joining', a
|
||||
await teamSync.reconcileNow('setup')
|
||||
assert.equal(activeMembers(1).length, 2, 'the members did land')
|
||||
assert.deepEqual(store.activity, [], 'and none of them was announced')
|
||||
// The same suppression, and the half where it matters more: a notification per
|
||||
// imported member would wake every phone in a 155-member guild at once.
|
||||
assert.deepEqual(store.tickles, [], 'and nobody was notified either')
|
||||
})
|
||||
|
||||
test('a roster run tickles ONCE per stream, however many members moved', async () => {
|
||||
provide(withMembers([member('0x1')]))
|
||||
await teamSync.reconcileNow('setup')
|
||||
|
||||
await resync(withMembers([
|
||||
member('0x1'),
|
||||
member('0x2', { displayName: 'Brenna' }),
|
||||
member('0x3', { displayName: 'Cael' }),
|
||||
]), 'test')
|
||||
|
||||
// Two joins, one tickle. The feed above records each of them, because it is a
|
||||
// record; the tickle says "something happened here" and is content-free, so a
|
||||
// second identical one carries no second piece of information.
|
||||
assert.equal(store.activity.length, 2)
|
||||
assert.deepEqual(store.tickles, ['member.joined'])
|
||||
})
|
||||
|
||||
test('a leadership change tickles its own stream, separately from joins', async () => {
|
||||
provide(withMembers([member('0x1'), member('0x2')]))
|
||||
await teamSync.reconcileNow('setup')
|
||||
|
||||
await resync({
|
||||
getTeams: async () => ({ ok: true, teams: [team('g1', 'A')] }),
|
||||
getTeamMembers: async () => ({ ok: true, complete: true, members: [member('0x1'), member('0x2')] }),
|
||||
getTeamLeaders: async () => ({ ok: true, leaders: ['0x2'] }),
|
||||
}, 'test')
|
||||
|
||||
assert.deepEqual(store.tickles, ['leadership.changed'])
|
||||
})
|
||||
|
||||
test('a member arriving after the first roster is announced', async () => {
|
||||
|
||||
Reference in New Issue
Block a user