Files
website/server/test/newsGump.test.js
Claude 55a3adea99 feat(news): auto-push published news to the in-game Town Cryer News gump (2.1)
Phase 4: sync the site's published news posts into the Protocol 2.1 News gump.

- uoLinkClient.postNews / deleteNews.
- utils/newsGump.js — a STATE SYNC (not a one-shot announce leg): an article
  stays in the gump while its post is published news and is pulled when it leaves
  that state. buildArticle renders a compact gump-HTML block (centred title +
  plain-text excerpt — the gump supports only a small HTML subset) with a
  "more info" link to /site/news and an optional gump image from the
  `news_gump_image` setting. Every call is best-effort / never-throws.
- Hooked into the posts pipeline alongside the existing announce enqueue:
  syncPost on create/update/publish (fresh publish announces; edits refresh
  silently; leaving published-news pulls the article), removePost on delete.
- reassertAll() runs in uoLinkSocket.backfill on every WS (re)connect —
  reconciles the gump to our source of truth and recovers any article whose
  original live push failed (silent, so a reconnect never re-proclaims old news).

Server 185/185, swagger regenerated. Refs .plans/protocol2-integration.md (Phase 4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:40:47 -05:00

70 lines
2.8 KiB
JavaScript

const { test, beforeEach, afterEach } = require('node:test')
const assert = require('node:assert/strict')
// Exercise the News-gump sync decisions against a fake sidecar client by
// monkeypatching the shared modules newsGump require()s (same instance) — no DB,
// no network.
const uoLinkClient = require('../src/utils/uoLinkClient')
const settings = require('../src/model/settings/settings.model')
const newsGump = require('../src/utils/newsGump')
let calls
const saved = {}
beforeEach(() => {
calls = { post: [], del: [] }
saved.postNews = uoLinkClient.postNews
saved.deleteNews = uoLinkClient.deleteNews
saved.get = settings.get
uoLinkClient.postNews = async (article) => { calls.post.push(article); return { ok: true, status: 200 } }
uoLinkClient.deleteNews = async (id) => { calls.del.push(id); return { ok: true, status: 200 } }
settings.get = async () => null // no gump image configured
})
afterEach(() => {
uoLinkClient.postNews = saved.postNews
uoLinkClient.deleteNews = saved.deleteNews
settings.get = saved.get
})
const newsPost = (over = {}) => ({ id: 42, category: 'news', published: true, title: 'Double XP Weekend', excerpt: 'Starts Friday.', body: null, ...over })
test('buildArticle centres the title, links the news list, and respects announce', async () => {
const a = await newsGump.buildArticle(newsPost(), { announce: false })
assert.equal(a.id, '42')
assert.match(a.body, /<CENTER>Double XP Weekend<\/CENTER>/)
assert.match(a.body, /Starts Friday\./)
assert.match(a.url, /\/site\/news$/)
assert.equal(a.announce, false)
})
test('a fresh publish into news pushes with announce=true', async () => {
await newsGump.syncPost(newsPost(), { wasPublished: false, wasNews: false })
assert.equal(calls.post.length, 1)
assert.equal(calls.post[0].announce, true)
assert.equal(calls.del.length, 0)
})
test('an edit of already-published news refreshes silently (announce=false)', async () => {
await newsGump.syncPost(newsPost({ title: 'Edited' }), { wasPublished: true, wasNews: true })
assert.equal(calls.post.length, 1)
assert.equal(calls.post[0].announce, false)
})
test('unpublishing published news pulls the article from the gump', async () => {
await newsGump.syncPost(newsPost({ published: false }), { wasPublished: true, wasNews: true })
assert.equal(calls.post.length, 0)
assert.deepEqual(calls.del, ['42'])
})
test('a draft never-published news post does nothing', async () => {
await newsGump.syncPost(newsPost({ published: false }), { wasPublished: false, wasNews: false })
assert.equal(calls.post.length, 0)
assert.equal(calls.del.length, 0)
})
test('a non-news post (e.g. screenshot) is never pushed', async () => {
await newsGump.syncPost(newsPost({ category: 'screenshot' }), { wasPublished: false, wasNews: false })
assert.equal(calls.post.length, 0)
})