fix(events): midnight in an announcement is 12:00 am on the Node we ship #200

Merged
whitlocktech merged 1 commits from fix/events-announce-midnight-hourcycle into edge 2026-09-10 00:28:05 +00:00
Member

This is the red X on #199, and on every events PR since #192. One test, the same one, in all eight runs — each reported # fail 1, so nothing else was hiding behind it.

not ok 586 - midnight reads as 12:00 am and never as 00:00
  The input did not match /12:00 am/. Input:
  'Sunday 13 September at 0:00 am (America/New_York)'

The defect is in production, not in the test

startsAtLabel asked Intl.DateTimeFormat('en-GB', { …, hour12: true }). That is not the same request as a 12-hour clock. For a locale whose default cycle is h23en-GB is one — hour12: true resolves to h11, whose hours run 0–11, so midnight renders 0:00 am.

hour12: true resolves to midnight
node:20-alpinewhat Dockerfile ships and CI runs h11 0:00 am
Node 24 — a dev machine h12 12:00 am

Same ICU (78.2) on both sides, so this is V8's ECMA-402 behaviour, not locale data — which is why no amount of matching the runner's locale would have found it. It is also why 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 — the exact contradiction the option was added to prevent, quoted in that comment.

The fix

hourCycle: 'h12' is the request that means what was meant. recurrence.js:71 already states the mirror-image rule (hourCycle: 'h23' rather than hour12: false, for the same reason in the other direction), and every other formatter in this repo and in module-uo uses hourCycle. announce.js was the only hour12 left in either tree; now there is none.

The comment beside it names the Node divergence, so the next person to read hourCycle does not "simplify" it back.

Verification, on the runtime that actually fails

Run under node:20-alpine against this tree:

without this change   not ok 19 - midnight reads as 12:00 am and never as 00:00   (# fail 1)
with it               2152 tests, 2060 pass, # fail 0
routes:manifest       route manifest up to date (280 routes)
engagement:manifest   engagement-triggers.json is current

On Node 22+ the test passes either way, so the test's comment now says a green run on a dev machine is not evidence and CI is what holds this line. 8:00 pm and the bad-zone/bad-instant cases are unchanged.

Two notes on what this is not: the other three red jobs in this cutover window were infrastructure, not code — link's and servuo-plugins' release runs died in the Gitea outage at 19:53–20:03 UTC (error: 530 pushing the tag; a Set up job that lost the server after 11m52s), and link's rust-gates failures were curl: (6) Could not resolve host: sh.rustup.rs inside the runner.

Onto edge so #199 picks it up (Phase 16b, step 2).

  • AI-assisted: written with Claude Code (Claude Opus 5).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

**This is the red X on `#199`, and on every events PR since `#192`.** One test, the same one, in all eight runs — each reported `# fail 1`, so nothing else was hiding behind it. ``` not ok 586 - midnight reads as 12:00 am and never as 00:00 The input did not match /12:00 am/. Input: 'Sunday 13 September at 0:00 am (America/New_York)' ``` ## The defect is in production, not in the test `startsAtLabel` asked `Intl.DateTimeFormat('en-GB', { …, hour12: true })`. **That is not the same request as a 12-hour clock.** For a locale whose default cycle is `h23` — `en-GB` is one — `hour12: true` resolves to **`h11`**, whose hours run 0–11, so midnight renders `0:00 am`. | | `hour12: true` resolves to | midnight | |---|---|---| | `node:20-alpine` — **what `Dockerfile` ships and CI runs** | `h11` | `0:00 am` | | Node 24 — a dev machine | `h12` | `12:00 am` | **Same ICU (78.2) on both sides**, so this is V8's ECMA-402 behaviour, not locale data — which is why no amount of matching the runner's locale would have found it. It is also why 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 — the exact contradiction the option was added to prevent, quoted in that comment. ## The fix `hourCycle: 'h12'` is the request that means what was meant. `recurrence.js:71` already states the mirror-image rule (`hourCycle: 'h23'` **rather than** `hour12: false`, for the same reason in the other direction), and every other formatter in this repo and in `module-uo` uses `hourCycle`. `announce.js` was the only `hour12` left in either tree; now there is none. The comment beside it names the Node divergence, so the next person to read `hourCycle` does not "simplify" it back. ## Verification, on the runtime that actually fails Run under `node:20-alpine` against this tree: ``` without this change not ok 19 - midnight reads as 12:00 am and never as 00:00 (# fail 1) with it 2152 tests, 2060 pass, # fail 0 routes:manifest route manifest up to date (280 routes) engagement:manifest engagement-triggers.json is current ``` On Node 22+ the test passes either way, so **the test's comment now says a green run on a dev machine is not evidence** and CI is what holds this line. `8:00 pm` and the bad-zone/bad-instant cases are unchanged. Two notes on what this is *not*: the other three red jobs in this cutover window were infrastructure, not code — `link`'s and `servuo-plugins`' release runs died in the Gitea outage at 19:53–20:03 UTC (`error: 530` pushing the tag; a `Set up job` that lost the server after 11m52s), and `link`'s `rust-gates` failures were `curl: (6) Could not resolve host: sh.rustup.rs` inside the runner. Onto `edge` so `#199` picks it up (Phase 16b, step 2). - [x] AI-assisted: written with Claude Code (Claude Opus 5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-10 00:21:13 +00:00
fix(events): midnight in an announcement is 12:00 am on the Node we ship
All checks were successful
PR Checks / client-build (pull_request) Successful in 40s
PR Checks / bot-tests (pull_request) Successful in 41s
PR Checks / server-tests (pull_request) Successful in 5m56s
7d7840eb6b
`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech approved these changes 2026-09-10 00:27:52 +00:00
whitlocktech merged commit baa4f7d5ba into edge 2026-09-10 00:28:05 +00:00
whitlocktech deleted branch fix/events-announce-midnight-hourcycle 2026-09-10 00:28:06 +00:00
Sign in to join this conversation.
No description provided.