// ── The public event surface (EVENTS_PLAN.md Phase 14a) ──────────────────── // // The phase's shipped claim: **a visitor with no account sees the calendar, one // event's page and an arc, and sees nothing an operator did not announce.** // // What is worth testing here is almost entirely the second half. The reads // themselves are joins; the decisions are about what is absent: // // • a rehearsal, an unlisted definition and a draft are absent from every // surface, and absent the same way — a 404 that cannot be told from a slug // that never existed // • the plan behind an event (phases, steps, actions, params) is never // published; a live run carries the LABEL of its phase and nothing else // • `failed` and `missed` are published as `cancelled`, because the difference // between them is about the deployment rather than about the event // • `member_key` never leaves the server, even on a results table // • a participant's own history obeys the same two exclusions as the calendar, // so attending an unannounced event does not disclose that it exists // // Stubbed at the `.db` layer, the shape `eventSchedule.test.js` uses. process.env.DB_HOST = '127.0.0.1' process.env.DB_PORT = '59999' const { test, beforeEach, afterEach, after } = require('node:test') const assert = require('node:assert/strict') const publicModel = require('../src/model/events/eventPublic.model') const definitionsDb = require('../src/model/events/eventDefinitions.db') const runsDb = require('../src/model/events/eventRuns.db') const seriesDb = require('../src/model/events/eventSeries.db') const versionsDb = require('../src/model/events/eventVersions.db') const participantsDb = require('../src/model/events/eventRunParticipants.db') const db = require('../src/utils/db') after(() => db.close()) const NOW = new Date('2026-09-01T12:00:00Z') const SPEC = { schedule: { kind: 'manual' }, phases: [ { id: 'muster', label: 'The muster', steps: [{ id: 's1', action: 'core.announce' }] }, { id: 'assault', label: 'The assault', steps: [] }, ], } const originals = {} for (const [name, mod] of [ ['definitionsDb', definitionsDb], ['runsDb', runsDb], ['seriesDb', seriesDb], ['versionsDb', versionsDb], ['participantsDb', participantsDb], ]) { originals[name] = { mod, fns: { ...mod } } } const restoreOriginals = () => { for (const { mod, fns } of Object.values(originals)) Object.assign(mod, fns) } let store const definition = (over = {}) => ({ id: 1, title: 'The Yew Invasion', slug: 'the-yew-invasion', summary: 'Orcish warbands are massing north of Yew.', body: '

They came at dusk.

', image_url: null, state: 'ready', listed: true, timezone: 'America/New_York', series_id: null, series_name: null, series_slug: null, current_version_id: 100, spec: SPEC, ...over, }) const run = (over = {}) => ({ id: 3692, definition_id: 1, version_id: 100, scope: '', status: 'completed', // Every one of these is a field the public shapes must NOT carry. They are on // the fixture on purpose: a `{ ...run }` anywhere in the model would publish // them, and the assertions below are what would catch it. health: 'degraded', cleanup_status: 'incomplete', claimed_by: 'worker-3', claim_expires_at: new Date(), last_error: 'sidecar responded 503', current_phase: 'assault', scheduled_for: new Date('2026-08-29T00:00:00Z'), started_at: new Date('2026-08-29T00:00:05Z'), ended_at: new Date('2026-08-29T01:30:00Z'), timezone: 'America/New_York', rehearsal: false, results_published_at: new Date('2026-08-29T02:00:00Z'), ...over, }) function installStubs() { definitionsDb.getPublicBySlug = async (slug) => { const d = store.definitions.find((x) => x.slug === slug) return d && d.state === 'ready' && d.listed ? d : undefined } definitionsDb.listPublicBySeries = async (seriesId) => store.definitions.filter((d) => d.series_id === seriesId && d.state === 'ready' && d.listed) definitionsDb.findSchedulable = async ({ listedOnly = false } = {}) => store.definitions .filter((d) => d.state === 'ready' && (!listedOnly || d.listed)) .map((d) => ({ ...d, version_spec: d.spec })) runsDb.listInWindow = async ({ from, to, publicOnly = false }) => store.runs.filter((r) => { const at = new Date(r.scheduled_for) if (at < from || at >= to) return false if (!publicOnly) return true const d = store.definitions.find((x) => x.id === r.definition_id) return !r.rehearsal && d && d.listed && d.state !== 'archived' }) runsDb.listPublicForDefinition = async (id) => store.runs .filter((r) => r.definition_id === id && !r.rehearsal) .sort((a, b) => new Date(b.scheduled_for) - new Date(a.scheduled_for)) seriesDb.getById = async (id) => store.series.find((s) => s.id === id) || null seriesDb.getBySlug = async (slug) => store.series.find((s) => s.slug === slug) || null versionsDb.getById = async (id) => (id === 100 ? { id, spec: SPEC } : null) participantsDb.listForRun = async () => store.participants participantsDb.listForUser = async () => store.history } beforeEach(() => { const d = definition() store = { definitions: [d], // The joined shape `listInWindow` answers with. runs: [{ ...run(), definition_title: d.title, definition_slug: d.slug }], series: [], participants: [], history: [], } installStubs() }) afterEach(restoreOriginals) // ── The calendar ─────────────────────────────────────────────────────────── test('a calendar entry carries no operational field at all', async () => { const result = await publicModel.calendar({ from: '2026-08-01', to: '2026-09-15', now: NOW }) assert.equal(result.ok, true) const [entry] = result.entries assert.equal(entry.title, 'The Yew Invasion') // The whole security property of this file, asserted positively: the entry has // exactly these keys and gaining one is a deliberate act. assert.deepEqual(Object.keys(entry).sort(), [ 'kind', 'live', 'scheduledFor', 'seriesName', 'seriesSlug', 'slug', 'status', 'timezone', 'title', ]) }) test('the calendar defaults to a month from now when no window is given', async () => { const result = await publicModel.calendar({ now: NOW }) assert.equal(result.ok, true) assert.equal(new Date(result.window.from).getTime(), NOW.getTime()) const days = (new Date(result.window.to) - new Date(result.window.from)) / 86_400_000 assert.equal(days, publicModel.DEFAULT_WINDOW_DAYS) }) test('a window wider than the cap is refused rather than served slowly', async () => { const result = await publicModel.calendar({ from: '2026-01-01', to: '2026-12-31', now: NOW }) assert.equal(result.ok, false) assert.equal(result.status, 400) }) test('a projection is not emitted for an instant a run already occupies', async () => { // The definition recurs weekly on the Saturday its one run already sits on. store.definitions[0].spec = { ...SPEC, schedule: { kind: 'weekly', days: ['saturday'], time: '00:00' }, } const result = await publicModel.calendar({ from: '2026-08-28', to: '2026-08-31', now: NOW }) const at = result.entries.filter( (e) => new Date(e.scheduledFor).getTime() === new Date('2026-08-29T00:00:00Z').getTime(), ) assert.equal(at.length, 1) assert.equal(at[0].kind, 'run') }) // ── What the public never sees ───────────────────────────────────────────── test('an unlisted event is absent from the calendar and 404s on its own page', async () => { store.definitions[0].listed = false const cal = await publicModel.calendar({ from: '2026-08-01', to: '2026-09-15', now: NOW }) assert.deepEqual(cal.entries, []) const page = await publicModel.event('the-yew-invasion') assert.equal(page.ok, false) assert.equal(page.status, 404) }) test('a draft answers exactly as an unlisted one does — indistinguishable from no such slug', async () => { store.definitions[0].state = 'draft' const draft = await publicModel.event('the-yew-invasion') const missing = await publicModel.event('no-such-event') assert.deepEqual(draft, missing) }) test('a rehearsal is absent from the calendar and from an event page', async () => { store.runs[0].rehearsal = true const cal = await publicModel.calendar({ from: '2026-08-01', to: '2026-09-15', now: NOW }) assert.deepEqual(cal.entries, []) const page = await publicModel.event('the-yew-invasion') assert.equal(page.ok, true) assert.deepEqual(page.event.past, []) }) test('an occurrence publishes no health, no cleanup state, no claim and no error', async () => { store.runs[0].status = 'running' const page = await publicModel.event('the-yew-invasion') const occurrence = page.event.current for (const leaked of ['health', 'cleanupStatus', 'claimedBy', 'claimExpiresAt', 'lastError', 'versionId']) { assert.equal(occurrence[leaked], undefined, `${leaked} must not be published`) } assert.equal(JSON.stringify(page).includes('sidecar responded 503'), false) }) test('a live run carries its phase LABEL, and never the spec behind it', async () => { store.runs[0].status = 'running' const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.current.phase, 'The assault') // The step ids in the fixture spec are the tell: if the spec were published // anywhere in this answer, this would find it. assert.equal(JSON.stringify(page).includes('core.announce'), false) }) test('a phase the pinned version does not name renders nothing rather than an id', async () => { store.runs[0].status = 'running' store.runs[0].current_phase = 'a-phase-since-renamed' const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.current.phase, null) }) test('failed and missed are both published as cancelled', async () => { for (const status of ['failed', 'missed']) { store.runs[0].status = status const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.past[0].status, 'cancelled', status) } }) test('paused is published as live — an operator holding a run is not a public state', async () => { store.runs[0].status = 'paused' const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.current.status, 'live') assert.equal(page.event.live, true) }) // ── Which side of now an occurrence falls on ─────────────────────────────── // // Both of these were found by the browser walk, and both are the same mistake: // the split reading a STATUS where it should read a clock. Dates here are // relative to the real clock, because `event()` asks `Date.now()` — a run // "next Friday" has to still be next Friday when this runs. const inDays = (n) => new Date(Date.now() + n * 86_400_000) test('a cancelled occurrence in the FUTURE is what is coming, not what happened', async () => { // It was announced and it has been called off, and "next Friday is off" is // exactly what somebody checking the calendar came to find out. Filing it // under "previously" tells them it already happened, which is the one thing // certainly untrue about it. store.runs = [ { ...run({ id: 1, status: 'cancelled', scheduled_for: inDays(4), ended_at: null, results_published_at: null }) }, { ...run({ id: 2, status: 'scheduled', scheduled_for: inDays(11), ended_at: null, results_published_at: null }) }, ] const page = await publicModel.event('the-yew-invasion') assert.deepEqual(page.event.upcoming.map((o) => o.runId), [1, 2]) assert.deepEqual(page.event.past, []) }) test('`next` skips a cancelled occurrence even though it is listed as coming', async () => { // The headline answers "when is the next one", and a cancelled occurrence is // not one. An event whose only future occurrence was called off has no `next` // and says so, while the cancellation is still listed below. store.runs = [ { ...run({ id: 1, status: 'cancelled', scheduled_for: inDays(4), ended_at: null, results_published_at: null }) }, ] const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.next, null) assert.equal(page.event.upcoming.length, 1) }) test('a scheduled occurrence whose moment has gone by is in the past', async () => { // The other direction of the same rule: the runner had not reached it, so its // status still says `scheduled` while the evening it named is over. store.runs = [ { ...run({ id: 1, status: 'scheduled', scheduled_for: inDays(-3), ended_at: null, results_published_at: null }) }, ] const page = await publicModel.event('the-yew-invasion') assert.deepEqual(page.event.upcoming, []) assert.deepEqual(page.event.past.map((o) => o.runId), [1]) }) // ── Results ──────────────────────────────────────────────────────────────── test('a results row publishes the score and the rank and never the member key', async () => { store.participants = [ { id: 1, member_key: 'serial:0x40001234', user_id: 7, score: 1420, rank_at: 1, meta: { name: 'Aldric' } }, ] const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.results.participants[0].name, 'Aldric') assert.equal(page.event.results.participants[0].rank, 1) assert.equal(JSON.stringify(page).includes('0x40001234'), false) assert.equal(JSON.stringify(page).includes('user_id'), false) }) test('an unpublished results table is absent rather than empty', async () => { store.runs[0].results_published_at = null store.participants = [{ id: 1, member_key: 'k', score: 10, rank_at: null, meta: null }] const page = await publicModel.event('the-yew-invasion') assert.equal(page.event.results, null) }) test('?run= selects which occurrence the results are about', async () => { const older = { ...run({ id: 3600, scheduled_for: new Date('2026-08-22T00:00:00Z') }), definition_title: 'The Yew Invasion', definition_slug: 'the-yew-invasion', } store.runs.push(older) const page = await publicModel.event('the-yew-invasion', { runId: '3600' }) assert.equal(page.event.results.runId, 3600) }) test('a run id belonging to no occurrence of this event renders the page anyway', async () => { // A stale link in a months-old mail should land on the event it was about, not // on a dead end. const page = await publicModel.event('the-yew-invasion', { runId: '999999' }) assert.equal(page.ok, true) assert.equal(page.event.slug, 'the-yew-invasion') }) // ── The arc ──────────────────────────────────────────────────────────────── test('a series with nothing listed in it is a 404, not an empty page', async () => { store.series = [{ id: 5, name: 'The Yew Campaign', slug: 'the-yew-campaign', description: null }] store.definitions[0].series_id = 5 store.definitions[0].listed = false const arc = await publicModel.series('the-yew-campaign') assert.equal(arc.ok, false) assert.equal(arc.status, 404) }) test('an arc lists its listed events and nothing about their plans', async () => { store.series = [{ id: 5, name: 'The Yew Campaign', slug: 'the-yew-campaign', description: 'An arc.' }] store.definitions[0].series_id = 5 const arc = await publicModel.series('the-yew-campaign') assert.equal(arc.ok, true) assert.deepEqual(Object.keys(arc.series.events[0]).sort(), ['imageUrl', 'slug', 'summary', 'title']) }) // ── Participation history ────────────────────────────────────────────────── test('history publishes the rank as null until results were published', async () => { store.history = [ { id: 9, run_id: 3692, score: 1420, rank_at: null, joined_at: new Date(), meta: null, scheduled_for: new Date('2026-08-29T00:00:00Z'), started_at: null, ended_at: null, status: 'completed', scope: '', timezone: 'UTC', results_published_at: null, definition_title: 'The Yew Invasion', definition_slug: 'the-yew-invasion', series_name: null, series_slug: null, }, ] const result = await publicModel.history(7) assert.equal(result.entries[0].rank, null) assert.equal(result.entries[0].resultsPublishedAt, null) assert.equal(result.entries[0].score, 1420) }) test('history publishes no member key', async () => { store.history = [ { id: 9, run_id: 3692, member_key: 'serial:0x40001234', score: 1, rank_at: 1, joined_at: new Date(), meta: null, scheduled_for: new Date(), status: 'completed', timezone: 'UTC', definition_title: 't', definition_slug: 's', }, ] const result = await publicModel.history(7) assert.equal(JSON.stringify(result).includes('0x40001234'), false) })