const { test } = require('node:test') const assert = require('node:assert/strict') const logic = require('../src/model/announceJobs/announceJobs.logic') // ── buildTownCrierText ─────────────────────────────────────────────────────── test('buildTownCrierText produces title, excerpt, and URL lines', () => { const lines = logic.buildTownCrierText( { id: 7, title: 'Server Update', excerpt: 'Big things afoot.', body: null }, { baseUrl: 'https://uom.example' }, ) assert.deepEqual(lines, ['Server Update', 'Big things afoot.', 'https://uom.example/site/news']) }) test('buildTownCrierText falls back to a stripped body when excerpt is empty', () => { const lines = logic.buildTownCrierText( { id: 1, title: 'T', excerpt: '', body: '
Hello world
' }, { baseUrl: 'https://uom.example' }, ) assert.equal(lines[1], 'Hello world') }) test('buildTownCrierText clamps each line to the sidecar per-line cap', () => { const longTitle = 'x'.repeat(500) const lines = logic.buildTownCrierText( { id: 1, title: longTitle, excerpt: 'y'.repeat(500), body: null }, { baseUrl: 'https://uom.example' }, ) for (const line of lines) assert.ok(line.length <= logic.MAX_LINE_LEN, `line too long: ${line.length}`) assert.ok(lines[0].endsWith('…')) assert.ok(lines.length <= logic.MAX_LINES) }) test('buildTownCrierText omits the excerpt line when there is no excerpt or body', () => { const lines = logic.buildTownCrierText( { id: 1, title: 'Only a title', excerpt: null, body: null }, { baseUrl: 'https://uom.example' }, ) assert.deepEqual(lines, ['Only a title', 'https://uom.example/site/news']) }) // ── classifyTownCrier ──────────────────────────────────────────────────────── test('classifyTownCrier: 2xx is done', () => { assert.equal(logic.classifyTownCrier({ ok: true, status: 200 }).outcome, 'done') }) test('classifyTownCrier: over-cap / auth / protocol errors are terminal (no retry)', () => { for (const status of [400, 401, 409]) { assert.equal(logic.classifyTownCrier({ ok: false, status }).outcome, 'terminal', `status ${status}`) } }) test('classifyTownCrier: shard-transient and network errors retry', () => { for (const status of [503, 504, 500, 0]) { assert.equal(logic.classifyTownCrier({ ok: false, status }).outcome, 'retry', `status ${status}`) } }) // ── classifyDiscord ────────────────────────────────────────────────────────── test('classifyDiscord: ok is done, every failure retries', () => { assert.equal(logic.classifyDiscord({ ok: true, status: 200 }).outcome, 'done') for (const status of [400, 503, 0]) { assert.equal(logic.classifyDiscord({ ok: false, status }).outcome, 'retry', `status ${status}`) } }) // ── scheduleAfter (backoff) ────────────────────────────────────────────────── test('scheduleAfter returns increasing delays then null at the attempt cap', () => { const d1 = logic.scheduleAfter(1) const d2 = logic.scheduleAfter(2) assert.ok(d1 > 0 && d2 > d1, 'delays should grow') // Exhausted once attempts reach MAX_ATTEMPTS. assert.equal(logic.scheduleAfter(logic.MAX_ATTEMPTS), null) assert.equal(logic.scheduleAfter(logic.MAX_ATTEMPTS + 3), null) }) // ── rollupStatus ───────────────────────────────────────────────────────────── test('rollupStatus derives the parent status from the two legs', () => { assert.equal(logic.rollupStatus('done', 'done'), 'done') assert.equal(logic.rollupStatus('failed', 'failed'), 'failed') assert.equal(logic.rollupStatus('pending', 'pending'), 'pending') // One terminal, the other not matching → partial. assert.equal(logic.rollupStatus('done', 'pending'), 'partial') assert.equal(logic.rollupStatus('pending', 'failed'), 'partial') assert.equal(logic.rollupStatus('done', 'failed'), 'partial') })