// ── Notifications and engagement (phase 10, PLAN.md §25) ─────────────────── // // The properties this suite holds, each with a failure behind it: // // • every declaration is one core will accept — a ceiling, a subjectKey that // names a declared variable, an example on every variable, a closed type — // because core refuses the WHOLE module at boot over one bad declaration; // • no trigger declares an address, or the raider (D66); // • the raid alert reaches exactly the authorised, linked people, one emit // each with `ownerUserId`, and nobody when there is no cupboard (D59, D67); // • a replayed event is told only while it is still news (D63); // • a clan notice goes to the clan and never to whoever caused it; // • a transition is announced once, and a first sighting never; // • a tie at the top of the leaderboard is not a new leader; // • every seed body interpolates only variables its trigger declares. const test = require('node:test') const assert = require('node:assert') const { fakeCtx, spy } = require('./_fakes') // Core's own check on a `url` variable (utils/engagementEmit.js RELATIVE_URL), // copied so a path this module builds is held to the rule it will meet. const RELATIVE_URL = /^\/(?!\/)[A-Za-z0-9\-._~/?#[\]@!$&'()*+,;=%]*$/ const CEILINGS = ['everyone', 'authenticated', 'subscribers', 'members', 'staff', 'admin', 'owner'] const TYPES = ['string', 'int', 'float', 'boolean', 'datetime', 'url'] const SERVER = { id: 'main', name: 'Main' } const NOW = Date.now() /** A fresh ctx, a recording emit, and a link table the tests control. */ function setup({ links = {}, members = {}, state = null, board = [] } = {}) { require('../core')._reset() const ctx = fakeCtx() require('../core').init(ctx) const emit = require('../engagement/emit') emit.reset() const linksDb = require('../model/links/links.db') const clansDb = require('../model/clans/clans.db') const clans = require('../model/clans/clans.model') const serversDb = require('../model/servers/servers.db') const eventsDb = require('../model/events/events.db') const originals = [ [linksDb, { ...linksDb }], [clansDb, { ...clansDb }], [clans, { ...clans }], [serversDb, { ...serversDb }], [eventsDb, { ...eventsDb }], ] linksDb.userIdsForSteamIds = async (ids) => ids.filter((id) => links[id]).map((id) => ({ steamId: id, userId: links[id] })) clans.resolveExternalId = async (serverId, frame) => (frame.clanId ? `${serverId}:${frame.clanId}:1` : null) clansDb.listMembers = async (externalId) => (members[externalId] || []).map((steamId) => ({ steamId })) serversDb.getState = async () => state eventsDb.leaderboard = async () => board.shift() || [] const restore = () => { for (const [mod, copy] of originals) Object.assign(mod, copy) } return { emit, calls: ctx.events.emit.calls, restore, eventsDb } } const raidFrame = (extra = {}) => ({ kind: 'entity.destroyed', t: NOW, ownerId: '100', prefab: 'door.hinged.metal', structure: 'door', attackerId: '900', attackerName: 'Raider', grid: 'H7', buildingId: '8113', authorized: [ { steamId: '101', online: false }, { steamId: '102', online: true }, { steamId: '103', online: false }, ], ...extra, }) // ── The declarations ─────────────────────────────────────────────────────── test('every trigger is a declaration core will accept', () => { const { TRIGGERS } = require('../engagement/triggers') const ids = new Set() for (const t of TRIGGERS) { assert.ok(t.id.startsWith('rust.'), `${t.id} is namespaced`) assert.ok(!ids.has(t.id), `${t.id} is declared once`) ids.add(t.id) assert.ok(CEILINGS.includes(t.ceiling), `${t.id} has a real ceiling (there is no "self")`) assert.ok(CEILINGS.includes(t.audience), `${t.id} has a real default audience`) assert.strictEqual(t.kind, 'event') const names = new Set(t.variables.map((v) => v.name)) assert.ok(names.has(t.subjectKey), `${t.id}'s subjectKey names a declared variable`) for (const v of t.variables) { assert.ok(TYPES.includes(v.type), `${t.id}.${v.name} has a closed type`) assert.ok(v.example !== undefined, `${t.id}.${v.name} has an example`) if (v.type === 'url') assert.match(v.example, RELATIVE_URL, `${t.id}.${v.name}'s example is site-relative`) } } }) test('the default audience is never wider than the ceiling', () => { const { TRIGGERS } = require('../engagement/triggers') // The pairs this catalogue uses, each one core's `permits` accepts. A new // pairing is a new line here — decided, not assumed. const ALLOWED = new Set(['everyone>subscribers', 'owner>owner', 'members>members', 'staff>staff']) for (const t of TRIGGERS) { assert.ok(ALLOWED.has(`${t.ceiling}>${t.audience}`), `${t.id}: ${t.audience} under ${t.ceiling}`) } }) test('no trigger declares an address, or who raided whom (D66)', () => { const { TRIGGERS } = require('../engagement/triggers') for (const t of TRIGGERS) { for (const v of t.variables) { // By camelCase WORD: `wipeId` contains the letters "ip" and is not one. const words = v.name.split(/(?=[A-Z])/).map((w) => w.toLowerCase()) for (const banned of ['ip', 'address', 'attacker', 'raider']) { assert.ok(!words.includes(banned), `${t.id}.${v.name}`) } } } }) test('the ceilings are the ones §25.2 decided', () => { const { TRIGGERS } = require('../engagement/triggers') const by = Object.fromEntries(TRIGGERS.map((t) => [t.id, t.ceiling])) assert.strictEqual(by['rust.base.destroyed'], 'owner') assert.strictEqual(by['rust.player.linked'], 'owner') for (const id of ['rust.player.reported', 'rust.player.banned', 'rust.player.unbanned', 'rust.login.denied']) { assert.strictEqual(by[id], 'staff', id) } for (const id of ['rust.clan.member.left', 'rust.clan.member.kicked', 'rust.clan.disbanded']) { assert.strictEqual(by[id], 'members', id) } // D64: core's team.member.joined already covers it, and kits wait for phase 13. assert.strictEqual(by['rust.clan.member.added'], undefined) assert.strictEqual(by['rust.kit.entitled'], undefined) }) test('a clan path survives core\'s url check, colons and all', () => { const { clanPath, serverPath, leaderboardPath } = require('../engagement/triggers') assert.match(clanPath('main:12:1790142840000'), RELATIVE_URL) assert.match(serverPath('eu 2'), RELATIVE_URL) assert.match(leaderboardPath('main'), RELATIVE_URL) }) test('every trigger names what happened and where, even with its optionals missing', () => { // The walk found a multi-server site's generic notice reading "A server came // online" — core's projection falls back to the LABEL when the payload carries // no `title`. Every trigger therefore declares its own, and the emitter writes // it; a headline that printed "undefined" would be worse than the label. const { TRIGGERS } = require('../engagement/triggers') const { headline } = require('../engagement/emit') for (const t of TRIGGERS) { const minimal = Object.fromEntries(t.variables.filter((v) => v.required).map((v) => [v.name, v.example])) const h = headline(t.id, minimal) assert.ok(h.title && h.intro, `${t.id} has a headline`) assert.ok(!/undefined|null/.test(h.title + h.intro), `${t.id}: ${h.title} / ${h.intro}`) } const online = headline('rust.server.online', { server: 'EU 2' }) assert.match(online.title, /EU 2/, 'the notice says WHICH server') }) // ── The seeds ────────────────────────────────────────────────────────────── test('every seeded rule is ours, off, and names bodies that exist', () => { const { TRIGGERS } = require('../engagement/triggers') const { TEMPLATES, RULE_GROUPS } = require('../engagement/seeds') const triggerIds = new Set(TRIGGERS.map((t) => t.id)) const own = new Set(TEMPLATES.map((t) => t.key)) const coreKeys = new Set(['notify.event', 'inapp.event', 'notify.digest']) const groupKeys = new Set() for (const group of RULE_GROUPS) { assert.ok(!groupKeys.has(group.key), `group ${group.key} is unique`) groupKeys.add(group.key) for (const r of group.rules) { assert.ok(triggerIds.has(r.trigger_id), `${r.trigger_id} is one of ours`) assert.strictEqual(r.enabled, undefined, 'enabled is never a seed parameter') assert.ok(Number.isInteger(r.max_sends_per_hour) && r.max_sends_per_hour >= 1) for (const [slot, key] of Object.entries(r.template_keys)) { assert.ok(own.has(key) || coreKeys.has(key), `${r.trigger_id}: ${key}`) if (slot !== 'digest') assert.ok(r.channels.includes(slot), `${r.trigger_id}: ${slot} is a channel`) } for (const channel of r.channels) { if (channel !== 'push') assert.ok(r.template_keys[channel], `${r.trigger_id}: a body for ${channel}`) } } } }) test('push appears only on the rules whose trigger is also a stream (D65)', () => { const { STREAMS } = require('../engagement/streams') const { RULE_GROUPS } = require('../engagement/seeds') const pushable = new Set(STREAMS.map((s) => s.id)) for (const group of RULE_GROUPS) { for (const r of group.rules) { if (r.channels.includes('push')) assert.ok(pushable.has(r.trigger_id), r.trigger_id) } } }) test('every body interpolates only what its trigger declares', () => { const { TRIGGERS } = require('../engagement/triggers') const { TEMPLATES } = require('../engagement/seeds') const declared = new Map(TRIGGERS.map((t) => [t.id, new Set(t.variables.map((v) => v.name))])) // Core's per-delivery and ambient variables — supplied by the renderer. const ambient = new Set(['unsubscribeUrl', 'siteName', 'siteUrl', 'logoUrl', 'year']) for (const t of TEMPLATES) { const vars = declared.get(t.triggerId) assert.ok(vars, `${t.key}'s trigger exists`) const text = JSON.stringify([t.subject, t.blocks]) for (const [, name] of text.matchAll(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g)) { assert.ok(vars.has(name) || ambient.has(name), `${t.key} uses {{${name}}}`) } assert.ok(t.key.startsWith('rust.')) if (t.channel === 'email') assert.ok(t.subject) else assert.strictEqual(t.subject, null) } }) test('the seeded raid rule is the OFFLINE raid alert, as a condition (D61)', () => { const { RULE_GROUPS } = require('../engagement/seeds') const raid = RULE_GROUPS.find((g) => g.key === 'raid-v1').rules[0] assert.deepStrictEqual(raid.conditions, { variable: 'ownerOnline', cmp: 'eq', value: false }) assert.strictEqual(raid.audience, 'owner') }) // ── The raid alert ───────────────────────────────────────────────────────── test('the raid alert reaches each authorised, linked person, and nobody else', async () => { const { emit, calls, restore } = setup({ links: { 101: 11, 102: 12 } }) try { const sent = await emit.onEvent(SERVER, { id: 1, kind: 'entity.destroyed', frame: raidFrame() }) assert.strictEqual(sent, 2) assert.deepStrictEqual(calls.map((c) => c[1].ownerUserId).sort(), [11, 12]) for (const [trigger, env] of calls) { assert.strictEqual(trigger, 'rust.base.destroyed') assert.strictEqual(env.recipientUserIds, undefined, 'owner-shaped, never a recipient list') assert.strictEqual(env.data.building, '8113') assert.strictEqual(env.data.structure, 'door') assert.strictEqual(env.data.atGrid, ' in H7') assert.ok(!JSON.stringify(env.data).includes('Raider'), 'the raider is never named') assert.ok(!JSON.stringify(env.data).includes('900')) } const online = Object.fromEntries(calls.map((c) => [c[1].ownerUserId, c[1].data.ownerOnline])) assert.deepStrictEqual(online, { 11: false, 12: true }) } finally { restore() } }) test('two Steam accounts held by one person are one alert, online if either is', async () => { const { emit, calls, restore } = setup({ links: { 101: 11, 102: 11 } }) try { await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame() }) assert.strictEqual(calls.length, 1) assert.strictEqual(calls[0][1].data.ownerOnline, true) } finally { restore() } }) test('an authorised attacker is demolishing their own base: no alert', async () => { const { emit, calls, restore } = setup({ links: { 101: 11, 102: 12 } }) try { await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ attackerId: '102' }) }) assert.strictEqual(calls.length, 0) } finally { restore() } }) test('no cupboard, nobody to tell — and a protocol-6 frame is the same (D67)', async () => { const { emit, calls, restore } = setup({ links: { 100: 10, 101: 11 } }) try { await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ buildingId: undefined, authorized: undefined }) }) // A protocol-6 frame: a BuildingBlock with an owner and no `authorized`. await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: { kind: 'entity.destroyed', t: NOW, ownerId: '100', prefab: 'wall' } }) assert.strictEqual(calls.length, 0, 'the placer is never a fallback') } finally { restore() } }) test('a replayed raid is still told for a day, and not after (D63)', async () => { const { emit, calls, restore } = setup({ links: { 101: 11 } }) try { await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ t: NOW - 3 * 3600 * 1000 }) }) assert.strictEqual(calls.length, 1) await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame({ t: NOW - 25 * 3600 * 1000 }) }) assert.strictEqual(calls.length, 1) } finally { restore() } }) test('the same frame replayed carries the same dedupe key, within core\'s bound', async () => { const { emit, calls, restore } = setup({ links: { 101: 11 } }) try { const item = { id: 7, kind: 'entity.destroyed', frame: raidFrame() } await emit.onEvent(SERVER, item) await emit.onEvent(SERVER, { ...item, id: 99 }) assert.strictEqual(calls[0][1].dedupeKey, calls[1][1].dedupeKey, 'keyed on the event, not the row id') assert.ok(calls[0][1].dedupeKey.length <= 190) } finally { restore() } }) // ── Broadcasts ───────────────────────────────────────────────────────────── test('a wipe is news for fifteen minutes (D63)', async () => { const { emit, calls, restore } = setup() try { await emit.onEvent(SERVER, { kind: 'server.wipe', frame: { kind: 'server.wipe', t: NOW - 60 * 1000, wipeId: 'w2' } }) await emit.onEvent(SERVER, { kind: 'server.wipe', frame: { kind: 'server.wipe', t: NOW - 20 * 60 * 1000, wipeId: 'w3' } }) assert.strictEqual(calls.length, 1) assert.strictEqual(calls[0][0], 'rust.wipe.started') assert.strictEqual(calls[0][1].data.wipeId, 'w2') } finally { restore() } }) test('online and offline are transitions, and a first sighting is not one', () => { const { emit, calls, restore } = setup() try { emit.serverObserved(SERVER, true) emit.serverObserved(SERVER, true) assert.strictEqual(calls.length, 0, 'a restart announces nothing') emit.serverObserved(SERVER, false) emit.serverObserved(SERVER, false) emit.serverObserved(SERVER, true) assert.deepStrictEqual(calls.map((c) => c[0]), ['rust.server.offline', 'rust.server.online']) assert.strictEqual(calls[0][1].data.serverId, 'main') } finally { restore() } }) test('a new leader is announced once; a tie is not a new leader', async () => { const row = (steamId, kills) => ({ steamId, name: `P${steamId}`, kills }) const { emit, calls, restore } = setup({ state: { wipeId: 'w1' }, board: [ [row('1', 5), row('2', 3)], // first sight: remembered, not announced [row('2', 5), row('1', 5)], // level on kills: not a change [row('2', 6), row('1', 5)], // strictly ahead: announced [row('2', 7), row('1', 5)], // the same leader: nothing ], }) try { for (let i = 0; i < 4; i += 1) await emit.checkLeader(SERVER) assert.strictEqual(calls.length, 1) assert.strictEqual(calls[0][0], 'rust.leaderboard.topped') assert.strictEqual(calls[0][1].data.leader, 'P2') assert.strictEqual(calls[0][1].data.kills, 6) } finally { restore() } }) // ── Clans ────────────────────────────────────────────────────────────────── test('a disband goes to the roster on the frame, not to the one who did it', async () => { const { emit, calls, restore } = setup({ links: { 201: 21, 202: 22, 203: 23 } }) try { await emit.onEvent(SERVER, { kind: 'clan.disbanded', frame: { kind: 'clan.disbanded', t: NOW, clanId: 4, clanName: 'Belt', steamId: '201', name: 'Boss', members: ['201', '202', '203'] }, }) assert.strictEqual(calls.length, 1) assert.strictEqual(calls[0][0], 'rust.clan.disbanded') assert.deepStrictEqual(calls[0][1].recipientUserIds.sort(), [22, 23]) assert.strictEqual(calls[0][1].data.by, 'Boss') assert.match(calls[0][1].data.clanUrl, RELATIVE_URL) } finally { restore() } }) test('the one kicked is told; the one who kicked is not', async () => { const { emit, calls, restore } = setup({ links: { 301: 31, 302: 32, 303: 33 }, members: { 'main:5:1': ['301', '302'] }, // the board already dropped 303 }) try { await emit.onEvent(SERVER, { kind: 'clan.member.kicked', frame: { kind: 'clan.member.kicked', t: NOW, clanId: 5, steamId: '303', name: 'Out', bySteamId: '301', byName: 'Boss' }, }) assert.deepStrictEqual(calls[0][1].recipientUserIds.sort(), [32, 33]) } finally { restore() } }) test('a leaver is not told they left, and a lone leaver tells nobody', async () => { const { emit, calls, restore } = setup({ links: { 401: 41, 402: 42 }, members: { 'main:6:1': ['401', '402'] } }) try { await emit.onEvent(SERVER, { kind: 'clan.member.left', frame: { kind: 'clan.member.left', t: NOW, clanId: 6, steamId: '402' } }) assert.deepStrictEqual(calls[0][1].recipientUserIds, [41]) await emit.onEvent(SERVER, { kind: 'clan.member.left', frame: { kind: 'clan.member.left', t: NOW, clanId: 7, steamId: '402' } }) assert.strictEqual(calls.length, 1) } finally { restore() } }) // ── Moderation ───────────────────────────────────────────────────────────── test('a ban never carries the address the frame does', async () => { const { emit, calls, restore } = setup() try { await emit.onEvent(SERVER, { kind: 'player.banned', frame: { kind: 'player.banned', t: NOW, steamId: '555', name: 'Cheater', ip: '203.0.113.9', reason: 'aimbot' }, }) assert.strictEqual(calls.length, 1) assert.ok(!JSON.stringify(calls[0][1]).includes('203.0.113.9')) assert.strictEqual(calls[0][1].data.reason, 'aimbot') } finally { restore() } }) test('an unapproved login becomes a staff notice, keyed on the attempt', async () => { const { emit, calls, restore, eventsDb } = setup() try { const asked = [] eventsDb.unapprovedLogins = async (q) => { asked.push(q) return [{ steamId: '777', t: NOW - 120000, name: 'Knocker' }] } const sent = await emit.sweepLoginDenied([SERVER], NOW) await emit.sweepLoginDenied([SERVER], NOW) assert.strictEqual(sent, 1) assert.strictEqual(asked[0].to, NOW - emit.LOGIN_APPROVAL_WINDOW_MS, 'an attempt waits its minute first') assert.strictEqual(calls[0][0], 'rust.login.denied') assert.strictEqual(calls[0][1].dedupeKey, calls[1][1].dedupeKey, 'a second sweep is a no-op in core') } finally { restore() } }) // ── The rest ─────────────────────────────────────────────────────────────── test('a new link tells its owner, and only its owner', () => { const { emit, calls, restore } = setup() try { emit.linked({ userId: 5, steamId: '76561198000000001', name: 'Me' }) assert.strictEqual(calls[0][0], 'rust.player.linked') assert.strictEqual(calls[0][1].ownerUserId, 5) assert.strictEqual(emit.linked({ userId: 0, steamId: 'x' }), 0) } finally { restore() } }) test('a frame the fan-out cannot handle costs one notice, never the caller', async () => { const { emit, restore } = setup() try { const linksDb = require('../model/links/links.db') linksDb.userIdsForSteamIds = async () => { throw new Error('database gone') } const sent = await emit.onEvent(SERVER, { kind: 'entity.destroyed', frame: raidFrame() }) assert.strictEqual(sent, 0) } finally { restore() } }) test('an audience that fails answers nobody, never everybody', async () => { require('../core')._reset() const ctx = fakeCtx({ db: { query: spy(() => Promise.reject(new Error('down'))), pool: {} } }) require('../core').init(ctx) const { AUDIENCES } = require('../engagement/audiences') for (const a of AUDIENCES) { assert.deepStrictEqual(await a.resolve({ clan: 'main:1:1', serverId: 'main' }), [], a.id) assert.deepStrictEqual(await a.resolve({}), [], `${a.id} with no param`) } })