// ── The integrations (EVENTS_PLAN.md Phase 10) ───────────────────────────── // // §J's three reuse claims, each turned into something that fails when the reuse // stops being real: // // • **`core.announce.post` reuses the announce legs** rather than growing a // second delivery pipeline — so the assertions are about what it puts in // `announce_jobs`, not about what reaches Discord. // • **an event's job must not stand on the news pipeline's toes.** A post may // now have two jobs, and everything that reads "the post's job" — the admin // panel, its retry button, `announced_at` — must still mean the news one. // This is the half that would be silent: nothing errors, the panel just // starts showing a different row. // • **`core.results.publish` is idempotent**, because it is an ordinary step // an author may place twice and the runner may retry. // // And the two seeded rules, checked against the declarations they name. A seeded // rule pointing at a template that does not exist, or at an audience its own // trigger's ceiling forbids, is a rule that fails on the first firing after an // operator switches it on — which is the worst possible moment to find out. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, after } = require('node:test') const assert = require('node:assert/strict') const registries = require('../src/modules/registries') const ceilings = require('../src/modules/ceilings') const coreRules = require('../src/engagement/coreRules') const templateSeeds = require('../src/engagement/templateSeeds') const { TRIGGERS } = require('../src/config/coreTriggers') const postsDb = require('../src/model/posts/posts.db') const postsModel = require('../src/model/posts/posts.model') const announceDb = require('../src/model/announceJobs/announceJobs.db') const announceModel = require('../src/model/announceJobs/announceJobs.model') const participantsDb = require('../src/model/events/eventRunParticipants.db') const runsDb = require('../src/model/events/eventRuns.db') const db = require('../src/utils/db') after(() => db.close()) // ── core.announce.post ───────────────────────────────────────────────────── const POSTS = { 1: { id: 1, category: 'news', title: 'The Yew Invasion', published: 1, announce_job_id: 9 }, 2: { id: 2, category: 'news', title: 'A draft', published: 0, announce_job_id: null }, 3: { id: 3, category: 'newsletter', title: 'Never announced', published: 1, announce_job_id: null }, } let enqueued let linked const originals = { getById: postsModel.getById, link: postsModel.linkAnnounceJob, create: announceDb.create, legIds: registries.announceLegIds, } beforeEach(() => { registries._reset() registries.registerCore() enqueued = [] linked = [] postsModel.getById = async (id) => POSTS[id] || null postsModel.linkAnnounceJob = async (id, jobId) => { linked.push({ id, jobId }) } announceDb.create = async (postId, legs, opts = {}) => { enqueued.push({ postId, legs, runId: opts.runId ?? null }) return 100 + enqueued.length } }) after(() => { postsModel.getById = originals.getById postsModel.linkAnnounceJob = originals.link announceDb.create = originals.create registries.announceLegIds = originals.legIds }) const announcePost = () => registries.eventAction('core.announce.post') test('a published post is queued on every registered leg, tagged with the run', async () => { const answer = await announcePost().perform({ runId: 3692, params: { postId: 1 }, verify: false }) assert.deepEqual(answer, { ok: true }) assert.equal(enqueued.length, 1) assert.equal(enqueued[0].postId, 1) assert.equal(enqueued[0].runId, 3692) // The legs come from the registry, so a module's leg is included without this // action naming one — which is the whole of "reuse the legs". assert.deepEqual(enqueued[0].legs, registries.announceLegIds()) }) test('an unpublished post is refused, and refused terminally', async () => { // A draft has no public page for a town-crier line to point at, and // announcing one would publish its title to a shard before an editor meant // to. Publishing is the CMS's decision and this action is not it. const answer = await announcePost().perform({ runId: 1, params: { postId: 2 }, verify: false }) assert.equal(answer.ok, false) assert.equal(answer.retry, false) assert.match(answer.error, /not published/) assert.equal(enqueued.length, 0) }) test('a post id that names nothing is refused terminally — it will not appear in sixty seconds', async () => { for (const bad of [999, 0, -1, 'twelve', null]) { const answer = await announcePost().perform({ runId: 1, params: { postId: bad }, verify: false }) assert.equal(answer.ok, false, `${bad}`) assert.equal(answer.retry, false, `${bad}`) } assert.equal(enqueued.length, 0) }) test('a dry run checks the post and queues nothing', async () => { const answer = await announcePost().perform({ runId: 1, params: { postId: 1 }, verify: true }) assert.deepEqual(answer, { ok: true }) assert.equal(enqueued.length, 0) // …and still refuses what the real run would refuse, which is the point of a // dry run: the cost of finding out is a page, not a half-changed world. const refused = await announcePost().perform({ runId: 1, params: { postId: 2 }, verify: true }) assert.equal(refused.ok, false) }) test('the post\'s back-pointer is left alone when it already has one', async () => { // `announce_job_id` is what the post admin panel reads and what // `shouldEnqueue` guards on. Moving it to an event's job would make a // re-published post announce itself again. await announceModel.enqueueForRun(1, 3692) assert.deepEqual(linked, []) }) test('a post that has never been announced gains the pointer, because this IS its announcement', async () => { await announceModel.enqueueForRun(3, 3692) assert.equal(linked.length, 1) assert.equal(linked[0].id, 3) }) // ── the news pipeline is untouched ───────────────────────────────────────── test('"the post\'s job" still means the news one, however many an event has added', async () => { // The half that would be silent: nothing errors, the admin panel just starts // rendering an event's job and its retry button retries that instead. // // Asserted against the query's own text rather than by stubbing the pool. // `announceJobs.db.js` destructures `query` at require time, so a stub on the // db module here would replace something nothing reads — and the test would // then pass whatever the SQL said, which is the one thing it exists to check. const source = require('node:fs').readFileSync( require.resolve('../src/model/announceJobs/announceJobs.db'), 'utf8', ) const fn = source.slice(source.indexOf('async function findByPostId')) assert.match(fn.slice(0, fn.indexOf('\n}')), /run_id IS NULL/) }) test('a run\'s job does not restamp the post\'s announced_at', async () => { // `announced_at` means "when this post was announced". An event linking a // three-week-old article would otherwise rewrite that to today, and the admin // panel would report a publication date the post does not have. const stamped = [] const saved = { findById: announceDb.findById, setStatus: announceDb.setStatus, markAnnounced: postsModel.markAnnounced } announceDb.findById = async (id) => ({ id, post_id: 1, run_id: id === 1 ? null : 3692, status: 'pending', legs: [{ leg: 'discord', status: 'done' }], }) announceDb.setStatus = async () => {} postsModel.markAnnounced = async (postId) => { stamped.push(postId) } try { await announceModel.refreshStatus(2) // a run's job assert.deepEqual(stamped, []) await announceModel.refreshStatus(1) // the news pipeline's own assert.deepEqual(stamped, [1]) } finally { Object.assign(announceDb, { findById: saved.findById, setStatus: saved.setStatus }) postsModel.markAnnounced = saved.markAnnounced } }) // ── core.results.publish ─────────────────────────────────────────────────── test('publishing ranks the run\'s participants and stamps it, and does neither on a dry run', async () => { const calls = [] const saved = { rank: participantsDb.rankRun, mark: runsDb.markResultsPublished } participantsDb.rankRun = async (id) => { calls.push(['rank', id]) return 3 } runsDb.markResultsPublished = async (id) => { calls.push(['stamp', id]) return true } try { const action = registries.eventAction('core.results.publish') assert.deepEqual(await action.perform({ runId: 3692, verify: true }), { ok: true }) assert.deepEqual(calls, []) assert.deepEqual(await action.perform({ runId: 3692, verify: false }), { ok: true }) assert.deepEqual(calls, [['rank', 3692], ['stamp', 3692]]) // Idempotent by construction: the ranking is a total order over // `(score, joined_at, id)`, so a second publication writes the same numbers. // That is what makes it safe as an ordinary retried step. await action.perform({ runId: 3692, verify: false }) assert.equal(calls.length, 4) } finally { participantsDb.rankRun = saved.rank runsDb.markResultsPublished = saved.mark } }) test('publishing takes no params — a run may not publish somebody else\'s results', async () => { assert.deepEqual(registries.eventAction('core.results.publish').params, []) }) test('publishing is `inspect`, so it is default-ON and an author can place it unaided', () => { // Nothing in the game world changes and nobody is messaged: a table core // already holds becomes readable. const action = registries.eventAction('core.results.publish') assert.equal(action.risk, 'inspect') assert.equal(action.reversible, 'none') }) // ── the option source ────────────────────────────────────────────────────── test('the posts option source answers value/label/group from published posts only', async () => { const saved = postsDb.listPublishedForOptions postsDb.listPublishedForOptions = async () => [ { id: 1, category: 'news', title: 'The Yew Invasion' }, { id: 3, category: 'newsletter', title: 'Never announced' }, ] try { // Through the resolver the catalog route uses, so the normalisation it // applies — every value stringified for the `