feat(events): publish runId on a public calendar run entry (Rust D125)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 39s
PR Checks / client-build (pull_request) Successful in 42s
PR Checks / server-tests (pull_request) Successful in 5m54s

A run entry on GET /public/events now names its run, the same id the
event page already publishes on each occurrence and `?run=` takes. A
Rust map marker carries core's run id and nothing else about its event,
so without this the app could only find the event by fetching every
event page.

A projected entry has no runId: nothing is committed to it. Rehearsals
and unlisted events stay absent from the calendar, so their markers
stay unlinked. The web calendar ignores the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-25 07:38:59 -05:00
parent 702ab89ae2
commit b5616f359c
4 changed files with 56 additions and 2 deletions

View File

@@ -164,10 +164,32 @@ test('a calendar entry carries no operational field at all', async () => {
// 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',
'kind', 'live', 'runId', 'scheduledFor', 'seriesName', 'seriesSlug', 'slug', 'status', 'timezone',
'title',
])
})
test('a run entry names its run, and a projection names none', async () => {
// Rust phase 15, D125: a map marker carries core's run id, and the app finds
// the event it belongs to from this calendar. The id is the one the event page
// already publishes on each occurrence.
store.definitions[0].spec = {
...SPEC,
schedule: { kind: 'weekly', days: ['saturday'], time: '00:00' },
}
const result = await publicModel.calendar({ from: '2026-08-28', to: '2026-09-15', now: NOW })
const runs = result.entries.filter((e) => e.kind === 'run')
const projected = result.entries.filter((e) => e.kind === 'projected')
assert.equal(runs.length, 1)
assert.equal(runs[0].runId, store.runs[0].id)
assert.ok(projected.length > 0, 'the weekly schedule must forecast past the one run')
for (const entry of projected) assert.equal('runId' in entry, false)
const page = await publicModel.event('the-yew-invasion')
const occurrences = [page.event.current, page.event.next, ...page.event.upcoming, ...page.event.past]
assert.ok(occurrences.some((o) => o && o.runId === runs[0].runId))
})
test('the default window reaches back as well as forward', async () => {
// §I: this route is "upcoming, live and recent". The default used to start at
// `now`, which left no room for the third word — an event that finished an hour