From 7d7840eb6b2fb3a7f0b5d01752e96357bf9137c3 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 9 Sep 2026 19:20:31 -0500 Subject: [PATCH] fix(events): midnight in an announcement is 12:00 am on the Node we ship `startsAtLabel` asked for `hour12: true` on `en-GB`. That is not the same request as a 12-hour clock, and it does not survive a Node upgrade: for a locale whose default cycle is h23, Node 20 resolves `hour12: true` to h11, whose hours run 0-11, so midnight renders "0:00 am". Node 22 and later resolve it to h12 and it renders "12:00 am". Same ICU on both sides, so it is V8's ECMA-402 behaviour rather than locale data. The image ships node:20-alpine and CI runs Node 20, while a dev machine is newer -- which is how this rendered correctly in front of everyone who wrote it and wrongly for every real recipient. An event mail announcing a midnight start said "0:00 am" while the schedule editor beside it said "12:00 AM": one instant, two spellings, which is the exact contradiction the option was added to prevent. `hourCycle: 'h12'` is the request that means what was meant. `recurrence.js` already states the mirror-image rule for `h23`, and every other formatter in this repo and in module-uo uses `hourCycle`; there is no `hour12` left here. This is the one test that has been red on every events PR since #192, and the only one -- each of those runs reported `# fail 1`. Verified by running the suite under node:20-alpine, where the test fails without this change and 2152 tests pass with it; on Node 22+ it passes either way, so the test's comment now says that a green run on a dev machine is not evidence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 --- server/src/events/announce.js | 13 ++++++++++++- server/test/eventAnnounce.test.js | 8 +++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/server/src/events/announce.js b/server/src/events/announce.js index e8d0f76..db52ade 100644 --- a/server/src/events/announce.js +++ b/server/src/events/announce.js @@ -67,7 +67,18 @@ function startsAtLabel(at, zone) { // Explicit rather than left to the locale, because `en-GB` would otherwise // render midnight as "00:00" while the schedule editor beside it writes // "12:00 AM" — one event, two spellings of the same instant. - hour12: true, + // + // `hourCycle: 'h12'` and NOT `hour12: true`, which is not the same request + // and does not survive a Node upgrade. For a locale whose default cycle is + // h23 — `en-GB` is one — Node 20 resolves `hour12: true` to **h11**, whose + // hours run 0–11, so midnight comes out "0:00 am"; Node 22 and later + // resolve it to h12 and it comes out "12:00 am". Same ICU on both, so this + // is V8's ECMA-402 behaviour and not locale data, and the image ships + // node:20-alpine while a dev machine is newer — which is how this rendered + // correctly in front of everyone who wrote it and wrongly for every real + // recipient. `recurrence.js` states the mirror-image rule for `h23`; there + // is no `hour12` left in this repo and it should stay that way. + hourCycle: 'h12', }).format(when) return `${text} (${zone || 'UTC'})` } catch { diff --git a/server/test/eventAnnounce.test.js b/server/test/eventAnnounce.test.js index bb72fc3..4d09e4c 100644 --- a/server/test/eventAnnounce.test.js +++ b/server/test/eventAnnounce.test.js @@ -260,9 +260,15 @@ test('the start time is written out in the SHARD\'s zone, not the server\'s', () }) test('midnight reads as 12:00 am and never as 00:00', () => { - // `hour12` is set explicitly. Left to the en-GB locale this would render + // The hour cycle is set explicitly. Left to the en-GB locale this would render // "00:00" while the schedule editor beside it writes "12:00 AM" — one event, // two spellings of the same instant. + // + // This assertion only has teeth on the Node the image ships (20), where + // `hour12: true` resolves to h11 and midnight reads "0:00 am". On Node 22+ it + // passes either way — so a green run on a dev machine is not evidence, and CI + // is what actually holds this line. See the note beside `hourCycle` in + // events/announce.js. assert.match(announce.startsAtLabel(new Date('2026-09-13T04:00:00Z'), 'America/New_York'), /12:00 am/) }) -- 2.49.1