fix(events): the public phase label finds the phase by key (runicnpc D314)
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 36s
PR Checks / client-build (pull_request) Successful in 7m50s
PR Checks / server-tests (pull_request) Successful in 13m50s

eventPublic.phaseLabel matched a run's current_phase against p.id, but spec.js
validates and the engine writes the phase's `key`, so every live run's public
page read "Under way" since Events Phase 14a, UO's events included. It now
matches `key` first and still accepts `id` second. The test fixture moves to
`key`, as real specs are, and a unit test covers a UO-shaped spec, the id
fallback, key-before-id and an unknown phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-06 16:57:46 -05:00
parent 0a37a44164
commit ef93c73d2c
2 changed files with 33 additions and 8 deletions

View File

@@ -92,16 +92,22 @@ const isLive = (status) => publicStatus(status) === 'live'
/**
* The label of the phase a run is in, resolved from the PINNED version's spec.
*
* A phase id is a slug an author typed and the label is what they meant it to
* read as, so a page rendering the id would show `phase-2` to the public. A
* A phase key is a slug an author typed and the label is what they meant it to
* read as, so a page rendering the key would show `phase-2` to the public. A
* phase the spec does not name answers null and the page shows nothing, which is
* the right answer for a version edited since: the run pinned the old spec and
* the old spec is what it is executing.
*
* **A run's `current_phase` is the phase's `key`**, which is what `spec.js`
* validates and the engine writes. This matched `id` until RunicNPC stage 9
* (D314), so every live run read "Under way" since Events Phase 14a; `id` is
* still accepted second, for a spec stored in that older shape.
*/
function phaseLabel(spec, phaseId) {
if (!phaseId || !spec || !Array.isArray(spec.phases)) return null
const phase = spec.phases.find((p) => p && p.id === phaseId)
return (phase && (phase.label || phase.id)) || null
function phaseLabel(spec, phaseKey) {
if (!phaseKey || !spec || !Array.isArray(spec.phases)) return null
const phase =
spec.phases.find((p) => p && p.key === phaseKey) || spec.phases.find((p) => p && p.id === phaseKey)
return (phase && (phase.label || phase.key || phase.id)) || null
}
/**

View File

@@ -40,8 +40,9 @@ 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: [] },
// `key`, as spec.js validates and every stored spec has it (D314).
{ key: 'muster', label: 'The muster', steps: [{ id: 's1', action: 'core.announce' }] },
{ key: 'assault', label: 'The assault', steps: [] },
],
}
@@ -368,6 +369,24 @@ test('only the live occurrence carries progress, asked for the reader (1.12.0)',
}
})
test('phaseLabel finds a phase by key first, still accepts id, and never shows a step', () => {
const { phaseLabel } = publicModel
// A UO event's shape: keyed phases, steps of UO's actions.
const uo = {
phases: [
{ key: 'gather', label: 'The gathering', steps: [{ action: 'uo.creature.spawn' }] },
{ key: 'siege', label: 'Siege of Yew', steps: [] },
],
}
assert.equal(phaseLabel(uo, 'gather'), 'The gathering')
assert.equal(phaseLabel(uo, 'siege'), 'Siege of Yew')
assert.equal(phaseLabel({ phases: [{ id: 'old', label: 'An older shape' }] }, 'old'), 'An older shape')
// A key and an id both matching: the key wins, because the key is what the engine writes.
assert.equal(phaseLabel({ phases: [{ id: 'x', label: 'By id' }, { key: 'x', label: 'By key' }] }, 'x'), 'By key')
assert.equal(phaseLabel(uo, 'nope'), null)
assert.equal(phaseLabel(uo, null), null)
})
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'