// ── Core's `news.post` emitter (ENGAGEMENT.md §7.1 Q9, Phase 11) ─────────── // // `news.post` was a declared payload contract with NO CALLER from Phase 2 until // this phase — a rule naming it could never fire, so on a real deployment the // only mail or inbox item a rule could produce came from Teams. This is the test // for the call that fixes it, and for the two things about it that are decisions // rather than plumbing: // // 1. The emit REPLACED `pushDispatch.publish('news.post', …)`, so news push now // rides a rule. Core seeds that rule DISABLED, which is why news push stops // on upgrade — deliberately, on the Phase 6 Team precedent. // 2. The seed carries its OWN one-shot key. The Team key is already stamped on // every deployment that has booted since Phase 6, and those are exactly the // deployments that lose their raw push — so joining that group would have // seeded the news rule on fresh installs only. const { test } = require('node:test') const assert = require('node:assert/strict') const registries = require('../src/modules/registries') const newsNotify = require('../src/utils/newsNotify') const coreRules = require('../src/engagement/coreRules') registries.registerCore() const POST = { id: 412, title: 'Five on Friday — the Yew invasion', excerpt: 'Four new champion spawns, and the fate of the Yew moongate.', category: 'Five on Friday', } test('a publish emits news.post with the declaration\'s own variables', () => { const ok = newsNotify.emitNewsPost(POST) assert.equal(ok, true) }) test('the emitted payload satisfies the declared contract', () => { // `emit` throws in dev on a payload that misses a required variable, so the // assertion that it did not throw above is already most of this. This says // which variables, so a future declaration change breaks here with a name. const declaration = registries.eventTrigger('news.post') const required = declaration.variables.filter((v) => v.required).map((v) => v.name) assert.deepEqual(required.sort(), ['postUrl', 'title']) }) test('postUrl is the news LIST, because the site has no per-post route', () => { // `App.jsx` mounts `/site/news` and nothing under it, which is why // `announceJobs.logic.js` links the list from the Discord and town-crier // announcements too. The declaration's `example` used to name `/news/`, // a path that 404s — and an example is what the template editor previews and // test-sends with, so a wrong one is a preview that looks right. assert.equal(newsNotify.NEWS_PATH, '/site/news') assert.ok(newsNotify.NEWS_PATH.startsWith('/'), 'site-relative, because it ends up in an href') assert.ok(!newsNotify.NEWS_PATH.startsWith('//'), 'not protocol-relative') const declaration = registries.eventTrigger('news.post') const postUrl = declaration.variables.find((v) => v.name === 'postUrl') assert.equal(postUrl.example, newsNotify.NEWS_PATH, 'the example is the value the emitter sends') }) test('a post with no excerpt falls back to the body, stripped of markup', () => { const text = newsNotify.excerptFrom({ body: '

Hello world

' }) assert.equal(text, 'Hello world') }) test('an empty post omits the excerpt rather than sending a blank one', () => { // `excerpt` is declared optional. An absent optional renders as absent; an // empty string renders as a blank line where a summary should be. assert.equal(newsNotify.excerptFrom({}), null) assert.equal(newsNotify.excerptFrom({ body: '

' }), null) }) test('a long body is truncated rather than reproduced in the mail', () => { const text = newsNotify.excerptFrom({ body: 'x'.repeat(500) }) assert.equal(text.length, newsNotify.EXCERPT_CHARS) assert.ok(text.endsWith('…')) }) test('a malformed post is a no-op, never an exception on the publish path', () => { // This runs inside `announceIfNewlyPublished`, after the post has been saved. // A notification that can fail the write behind it is a defect. assert.equal(newsNotify.emitNewsPost(null), false) assert.equal(newsNotify.emitNewsPost({}), false) }) // ── The seed, and why it is its own group ────────────────────────────────── test('the news rule is seeded, disabled, on all three channels', () => { assert.equal(coreRules.NEWS_RULES.length, 1) const [rule] = coreRules.NEWS_RULES assert.equal(rule.trigger_id, 'news.post') assert.equal(rule.audience, 'subscribers') // **Push is on this rule** because push is what the raw tickle did. Leaving it // off would mean an operator who enabled the rule to restore news push got // mail instead. assert.deepEqual(rule.channels, ['email', 'inapp', 'push']) // Nothing core seeds is ever enabled — `seedGroup` stamps `enabled: 0` over // every entry, so the rule cannot ship on even by accident. assert.equal(rule.enabled, undefined) }) test('the news seed has its own one-shot key, separate from the Team group\'s', () => { // The failure this prevents: the Team key is already stamped on every // deployment that has booted since Phase 6, and the guard reads its presence. // Appending to the Team list would have seeded the news rule on fresh installs // only — and on exactly the upgrades that lose their raw news push, never. assert.notEqual(coreRules.NEWS_SEEDED_KEY, coreRules.SEEDED_KEY) assert.equal(coreRules.SEEDED_KEY, 'engagement_team_rules_seeded') assert.equal(coreRules.NEWS_SEEDED_KEY, 'engagement_news_rule_seeded') }) test('the audience the rule names is one the trigger\'s ceiling permits', () => { const ceilings = require('../src/modules/ceilings') const declaration = registries.eventTrigger('news.post') for (const rule of [...coreRules.RULES, ...coreRules.NEWS_RULES]) { const d = registries.eventTrigger(rule.trigger_id) assert.ok(d, `${rule.trigger_id} is declared`) assert.ok( ceilings.permits(d.ceiling, rule.audience), `${rule.trigger_id}: audience "${rule.audience}" is within ceiling "${d.ceiling}"`, ) } // Named explicitly, because it is the one seeded rule whose ceiling is wider // than its audience: a rule editor may widen news to `authenticated`, and // deliberately may not widen a Team event past `members`. assert.equal(declaration.ceiling, 'authenticated') })