From 89c179e412098bbf24adb76d5d81dfc01bfe5056 Mon Sep 17 00:00:00 2001 From: Elie Habib Date: Tue, 21 Apr 2026 13:46:21 +0400 Subject: [PATCH] fix(brief): cover greeting was hardcoded "Good evening" regardless of issue time (#3254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(brief): cover greeting was hardcoded "Good evening" regardless of issue time Reported: a brief viewed at 13:02 local time showed "Good evening" on the cover (slide 1) but "Good afternoon." on the digest greeting page (slide 2). Cause: `server/_shared/brief-render.js:renderCover` had the string `'Good evening'` hardcoded in the cover's mono-cased salutation slot. The digest greeting page (slide 2) renders the time-of-day-correct value from `envelope.data.digest.greeting`, which is computed by `shared/brief-filter.js:174-179` from `localHour` in the user's TZ (< 12 → morning, < 18 → afternoon, else → evening). So any brief viewed outside the literal evening showed an inconsistent pair. Fix: thread `digest.greeting` into `renderCover`; a small `coverGreeting()` helper strips the trailing period so the cover's no-punctuation mono style is preserved. On unexpected/missing values it falls back to a generic "Hello" rather than silently re-hardcoding a specific time of day. Tests: 5 regression cases in `tests/brief-magazine-render.test.mjs` cover afternoon/morning/evening parity, period stripping, and HTML escape (defense-in-depth). 60 total in that file pass. Full test:data 5921 pass. typecheck + typecheck:api + biome clean. * chore(brief): fix orphaned JSDoc on coverGreeting / renderCover Greptile flagged: the original `renderCover` JSDoc block stayed above `coverGreeting` when the helper was inserted, so the @param shape was misattributed to the wrong function and `renderCover` was left undocumented (plus the new `greeting` field was unlisted). Moved the opts-shape JSDoc to immediately above `renderCover` and added `greeting: string` to the param type. `coverGreeting` keeps its own prose comment. No runtime change. --- server/_shared/brief-render.js | 19 +++++++-- tests/brief-magazine-render.test.mjs | 64 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/server/_shared/brief-render.js b/server/_shared/brief-render.js index 8a3cf18e5..294ad8b61 100644 --- a/server/_shared/brief-render.js +++ b/server/_shared/brief-render.js @@ -377,9 +377,21 @@ function digestRunningHead(dateShort, label) { // ── Page renderers ─────────────────────────────────────────────────────────── /** - * @param {{ dateLong: string; issue: string; storyCount: number; pageIndex: number; totalPages: number }} opts + * Strip the trailing period from envelope.data.digest.greeting + * ("Good afternoon." → "Good afternoon") so the cover's mono-cased + * salutation stays consistent with the historical no-period style. + * Defensive: if the envelope ever produces an unexpected value, fall + * back to a generic "Hello" rather than hardcoding a wrong time-of-day. */ -function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages }) { +function coverGreeting(greeting) { + if (typeof greeting !== 'string' || greeting.length === 0) return 'Hello'; + return greeting.replace(/\.+$/, '').trim() || 'Hello'; +} + +/** + * @param {{ dateLong: string; issue: string; storyCount: number; pageIndex: number; totalPages: number; greeting: string }} opts + */ +function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages, greeting }) { const blurb = storyCount === 1 ? 'One thread that shaped the world today.' @@ -399,7 +411,7 @@ function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages }) { `

${escapeHtml(blurb)}

` + '' + '
' + - 'Good evening' + + `${escapeHtml(coverGreeting(greeting))}` + 'Swipe / ↔ to begin' + '
' + `
${pad2(pageIndex)} / ${pad2(totalPages)}
` + @@ -1200,6 +1212,7 @@ export function renderBriefMagazine(envelope, options = {}) { storyCount: stories.length, pageIndex: ++p, totalPages, + greeting: digest.greeting, }), ); diff --git a/tests/brief-magazine-render.test.mjs b/tests/brief-magazine-render.test.mjs index 582e74952..b0fd91169 100644 --- a/tests/brief-magazine-render.test.mjs +++ b/tests/brief-magazine-render.test.mjs @@ -700,3 +700,67 @@ describe('renderBriefMagazine — publicMode', () => { assert.equal(a, b); }); }); + +// ── Regression: cover greeting follows envelope.data.digest.greeting ───────── +// Previously the cover hardcoded "Good evening" regardless of issue time, so +// a brief composed at 13:02 local (envelope greeting = "Good afternoon.") +// rendered "Good evening" on the cover and "Good afternoon." on slide 2 — +// visibly inconsistent. Fix wires digest.greeting into the cover (period +// stripped for the mono-cased slot). +describe('cover greeting ↔ digest.greeting parity', () => { + /** + * Extract the cover
so we can assert on it in isolation without + * matching the identical greeting that appears on slide 2. + */ + function extractCover(html) { + const match = html.match(/
[\s\S]*?<\/section>/); + assert.ok(match, 'cover section must be present'); + return match[0]; + } + + it('renders "Good afternoon" on the cover when digest.greeting is "Good afternoon."', () => { + const env = envelope({ + digest: { ...envelope().data.digest, greeting: 'Good afternoon.' }, + }); + const cover = extractCover(renderBriefMagazine(env)); + assert.ok(cover.includes('>Good afternoon<'), `cover should contain "Good afternoon" without period, got: ${cover}`); + assert.ok(!cover.includes('Good evening'), 'cover must NOT say "Good evening" when digest.greeting is afternoon'); + }); + + it('renders "Good morning" on the cover when digest.greeting is "Good morning."', () => { + const env = envelope({ + digest: { ...envelope().data.digest, greeting: 'Good morning.' }, + }); + const cover = extractCover(renderBriefMagazine(env)); + assert.ok(cover.includes('>Good morning<')); + assert.ok(!cover.includes('Good evening')); + assert.ok(!cover.includes('Good afternoon')); + }); + + it('renders "Good evening" on the cover when digest.greeting is "Good evening."', () => { + const env = envelope({ + digest: { ...envelope().data.digest, greeting: 'Good evening.' }, + }); + const cover = extractCover(renderBriefMagazine(env)); + assert.ok(cover.includes('>Good evening<')); + }); + + it('strips trailing period(s) — cover is mono-cased, no punctuation', () => { + const env = envelope({ + digest: { ...envelope().data.digest, greeting: 'Good afternoon...' }, + }); + const cover = extractCover(renderBriefMagazine(env)); + // Envelope can send any trailing dot count; cover strips all of them. + assert.ok(cover.includes('>Good afternoon<')); + assert.ok(!cover.includes('Good afternoon.')); + }); + + it('HTML-escapes the greeting (defense-in-depth, even though envelope values are controlled)', () => { + const env = envelope({ + digest: { ...envelope().data.digest, greeting: '.' }, + }); + const cover = extractCover(renderBriefMagazine(env)); + assert.ok(!cover.includes('