diff --git a/src/__tests__/timezone-explicit-formatter-guard.test.ts b/src/__tests__/timezone-explicit-formatter-guard.test.ts index 3e8f01d31..553a6a971 100644 --- a/src/__tests__/timezone-explicit-formatter-guard.test.ts +++ b/src/__tests__/timezone-explicit-formatter-guard.test.ts @@ -93,7 +93,18 @@ function bareFormatterSites(): Array<{ file: string; line: number }> { pattern.lastIndex = 0; for (let m = pattern.exec(source); m !== null; m = pattern.exec(source)) { const args = argumentList(source, m.index + m[0].length - 1); - if (args.includes("timeZone")) continue; + // The property, not the substring, and both ways of writing it. + // + // `timeZoneName: "short"` is a DISPLAY option — it appends "GMT+1" to + // the output and leaves the zone as the browser's. A plain substring + // check waves it through, and the result then looks MORE + // timezone-aware than a bare formatter while being exactly as wrong. + // + // The shorthand `{ timeZone }` is the common form in this tree (nine + // correct call sites use it), so a colon-only check would report all + // of them and the honest response would be to allowlist them — which + // is how a guard stops guarding. + if (/\btimeZone\s*(?::|,|\}|$)/m.test(args)) continue; hits.push({ file, line: source.slice(0, m.index).split("\n").length, @@ -144,6 +155,23 @@ describe("date formatters name the calendar they render in", () => { } }); + it("does not accept timeZoneName as a substitute for timeZone", () => { + // `timeZoneName: "short"` appends "GMT+1" to the output and leaves the + // zone as the browser's. The first version of this guard tested for the + // substring "timeZone", which that satisfies — so a formatter that LOOKS + // more timezone-aware than a bare one sailed through while being exactly + // as wrong. An independent read found it; this pins it. + const property = /\btimeZone\s*(?::|,|\}|$)/m; + expect(property.test('{ hour: "2-digit", timeZoneName: "short" }')).toBe( + false, + ); + expect(property.test('{ hour: "2-digit", timeZone: tz }')).toBe(true); + expect(property.test("{ timeZone : tz }")).toBe(true); + // The shorthand forms, which are what most of this tree actually writes. + expect(property.test("{ timeZone, year: 'numeric' }")).toBe(true); + expect(property.test("{ ...options, timeZone }")).toBe(true); + }); + it("carries no allowlist entry that no longer has a bare formatter", () => { // A stale exception is worse than none: it documents a decision about code // that has moved on, and it quietly covers whatever lands in that file next. diff --git a/src/lib/export/backup-plan.ts b/src/lib/export/backup-plan.ts index 11c908906..e65a4b572 100644 --- a/src/lib/export/backup-plan.ts +++ b/src/lib/export/backup-plan.ts @@ -154,7 +154,7 @@ export const DERIVED_MODELS: Readonly> = { DocumentContentIndex: "A search index over `InboundDocument` text; rebuilt by the indexer.", DocumentThumbnail: - "Rendered from the stored document bytes on demand, and those bytes are carried, so the picture rebuilds itself on first view.", + "Rendered from the stored document bytes on demand. Those bytes travel in a disaster-recovery payload, so the picture rebuilds itself on first view; a portable export carries metadata only, and there the thumbnail has nothing to rebuild from — but neither does the document, so nothing is lost that the export did not already disclose.", ArrivalReaction: "Derived from what changed since the last visit; recomputed per visit.", DismissedPriorityItem: @@ -206,7 +206,7 @@ export const NOT_IN_BACKUP_MODELS: Readonly> = { // Ledgers whose meaning is local to the deployment. PushAttempt: - "A 90-day delivery ledger for this host's sends. It documents what happened here.", + "A 90-day delivery ledger for this host's sends, and since v1.33.1 also the anchor rows the reminder dedup writes under a sentinel channel. Both describe what this instance did; restoring either onto another host would assert sends it never made, and the dedup anchors would suppress reminders that host still owes.", AuditLog: "An append-only record of actions taken on this instance. Merging one instance's audit trail into another's would make the trail untrue.", CoachUsage: