From e6c994a69e9bd70d2efdd73142fefc1da2c0798f Mon Sep 17 00:00:00 2001 From: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Date: Fri, 21 Aug 2026 20:05:01 -0700 Subject: [PATCH] fix(desktop): pin the transcript baseline fixture's locale too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pinning TZ fixed one ambient input to the captured-markup fixture and missed a second one. The session-boundary divider formats its date with a bare `toLocaleString()` (`AgentSessionTranscriptChrome.tsx`), which is locale-sensitive as well as zone-sensitive: the fixture holds "6/14/2026, 7:05:00 PM", and under `LANG=de-DE` the same render produces "14.6.2026, 19:05:00". Node takes its default locale from LANG/LC_ALL, so the byte-for-byte comparison failed on any machine with a non-US locale — the same class of bug as the timezone one, one variable over, and the TZ guard passed happily while it happened. Assigning `process.env.LANG` at runtime does not fix this: node resolves its default locale once at startup, so unlike `TZ` (which `Date` reads lazily) the assignment is silently ineffective. Pinning the locale means overriding the two formatting surfaces the render path reaches — `Intl.DateTimeFormat` constructed without an explicit locale, and `Date.prototype.toLocale*`, which does not route through `Intl.DateTimeFormat` and needs its own patch. `Intl.DateTimeFormat` must be replaced with a plain function, not an arrow: the render path calls it with `new`, and an arrow function is not a constructor. The guard assertion now covers locale and `toLocaleString` output alongside the zone, so a future drift fails with a specific message instead of a 12KB markup diff. Verified: the de-DE failure reproduces before this change, and the suite passes after it under TZ/LANG combinations UTC/en-US, UTC/de-DE, America/New_York/ja-JP, Asia/Tokyo/de-DE, and UTC with LANG unset. Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz> Co-authored-by: Bradley Axen Signed-off-by: Bradley Axen --- ...essionTranscriptList.conversation.test.mjs | 69 ++++++++++++++++--- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs index 28e7eb917c..e1c5378141 100644 --- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs +++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs @@ -20,15 +20,54 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { after, afterEach, before, test } from "node:test"; -// The captured markup embeds locally-formatted timestamps ("… at 7:00:01 PM"), -// because `formatTranscriptTimestampTitle` formats in the ambient zone. Pin the -// zone so the fixture is reproducible on any developer machine and in CI (which -// runs UTC) — otherwise the byte-for-byte comparison fails purely on the -// capturer's offset. Must be set before the transcript modules are imported: -// their `Intl.DateTimeFormat` instances are module-level constants that resolve -// the zone once, at construction. +// The captured markup embeds formatted dates and times, so the fixture is only +// reproducible if every ambient formatting input is pinned. Two of them bite: +// +// - **Zone.** `formatTranscriptTimestampTitle` formats in the ambient zone +// ("… at 7:00:01 PM"), so a capture at UTC-7 fails against CI's UTC. +// - **Locale.** The session-boundary divider uses a bare `toLocaleString()` +// (`AgentSessionTranscriptChrome.tsx`), which is locale-sensitive as well as +// zone-sensitive: "6/14/2026, 7:05:00 PM" becomes "14.6.2026, 19:05:00" +// under de-DE. Node derives its default locale from LANG/LC_ALL, so this +// varies by machine independently of the zone. +// +// `TZ` can be set here because `Date` reads it lazily. The locale CANNOT: node +// resolves its default locale once at startup, so assigning `process.env.LANG` +// at runtime has no effect (verified — it silently keeps the startup locale). +// Pinning it therefore means overriding the two formatting surfaces the render +// path can reach: `Intl.DateTimeFormat` when constructed with no explicit +// locale, and `Date.prototype.toLocale*`, which does NOT route through +// `Intl.DateTimeFormat` and so needs its own patch. +// +// All of this must happen before the transcript modules are imported: their +// `Intl.DateTimeFormat` instances are module-level constants that resolve zone +// and locale once, at construction. process.env.TZ = "UTC"; +const FIXTURE_LOCALE = "en-US"; +const OriginalDateTimeFormat = Intl.DateTimeFormat; +// A plain function, not an arrow: the render path calls +// `new Intl.DateTimeFormat(...)`, and an arrow function is not a constructor. +// Returning a genuine instance keeps `new`, plain calls, and `instanceof` all +// working. +function LocalePinnedDateTimeFormat(locales, options) { + return new OriginalDateTimeFormat(locales ?? FIXTURE_LOCALE, options); +} +LocalePinnedDateTimeFormat.prototype = OriginalDateTimeFormat.prototype; +LocalePinnedDateTimeFormat.supportedLocalesOf = + OriginalDateTimeFormat.supportedLocalesOf.bind(OriginalDateTimeFormat); +Intl.DateTimeFormat = LocalePinnedDateTimeFormat; +for (const method of [ + "toLocaleString", + "toLocaleDateString", + "toLocaleTimeString", +]) { + const original = Date.prototype[method]; + Date.prototype[method] = function (locales, options) { + return original.call(this, locales ?? FIXTURE_LOCALE, options); + }; +} + import { JSDOM } from "jsdom"; const BASELINE_MARKUP = JSON.parse( @@ -740,14 +779,24 @@ test("default and compactPreview markup is unchanged by the conversation variant }); test("the byte-for-byte baseline actually exercises every renderable item kind", async () => { - // The fixture embeds formatted timestamps, so a zone other than the one it was - // captured in fails the comparison above for a reason that has nothing to do - // with markup. Fail loudly and specifically instead. + // The fixture embeds formatted dates and times, so a zone or locale other than + // the one it was captured in fails the comparison above for a reason that has + // nothing to do with markup. Fail loudly and specifically instead. assert.equal( new Intl.DateTimeFormat().resolvedOptions().timeZone, "UTC", "the baseline fixture is captured in UTC — see the TZ pin at the top of this file", ); + assert.equal( + new Intl.DateTimeFormat().resolvedOptions().locale, + FIXTURE_LOCALE, + `the baseline fixture is captured in ${FIXTURE_LOCALE} — see the locale pin at the top of this file`, + ); + assert.equal( + new Date("2026-06-14T19:05:00.000Z").toLocaleString(), + "6/14/2026, 7:05:00 PM", + "Date.prototype.toLocaleString must be locale-pinned too — the session-boundary divider uses it", + ); // Guards the fixture itself: a baseline that silently stopped covering a kind // would keep passing the comparison above while protecting nothing. Asserted // against `default`, which renders every kind.