diff --git a/src/__tests__/sql-now-utc-guard.test.ts b/src/__tests__/sql-now-utc-guard.test.ts index b64c5e313..cf7b5baa3 100644 --- a/src/__tests__/sql-now-utc-guard.test.ts +++ b/src/__tests__/sql-now-utc-guard.test.ts @@ -20,15 +20,36 @@ import { describe, expect, it } from "vitest"; const ROOT = join(__dirname, "..", ".."); const SRC = join(ROOT, "src"); -/** `file: reason` for every bare clock read that is deliberately left. */ -const ALLOWED: Record = { - "src/lib/rollups/mood-rollups.ts": "computed_at is timestamptz", - "src/lib/rollups/medication-compliance-rollups.ts": - "computed_at is timestamptz", - "src/lib/jobs/job-failures.ts": "pg-boss tables are timestamptz", - "src/lib/jobs/heavy-lane.ts": "pg-boss start_after is timestamptz", - "src/lib/jobs/reminder/registrar-shared.ts": "pg-boss tables are timestamptz", - "src/lib/db-compat.ts": "DDL default on a timestamptz column", +/** + * Every bare clock read that is deliberately left: the file, how many such + * reads it holds, and why. The count makes the exception cover those reads + * and not the next one written into the same file. + */ +const ALLOWED: Record = { + "src/lib/rollups/mood-rollups.ts": { + hits: 1, + why: "computed_at is timestamptz", + }, + "src/lib/rollups/medication-compliance-rollups.ts": { + hits: 1, + why: "computed_at is timestamptz", + }, + "src/lib/jobs/job-failures.ts": { + hits: 2, + why: "pg-boss tables are timestamptz", + }, + "src/lib/jobs/heavy-lane.ts": { + hits: 1, + why: "pg-boss start_after is timestamptz", + }, + "src/lib/jobs/reminder/registrar-shared.ts": { + hits: 1, + why: "pg-boss tables are timestamptz", + }, + "src/lib/db-compat.ts": { + hits: 1, + why: "DDL default on a timestamptz column", + }, }; const SQL_CLOCK = @@ -47,11 +68,11 @@ function walk(dir: string, out: string[] = []): string[] { return out; } -function offenders(): string[] { +/** Every bare clock read in the tree, allowed or not. */ +function bareReads(): string[] { const found: string[] = []; for (const file of walk(SRC)) { const rel = relative(ROOT, file); - if (ALLOWED[rel]) continue; readFileSync(file, "utf8") .split("\n") .forEach((line, index) => { @@ -81,18 +102,22 @@ describe("raw SQL clock reads are anchored to UTC", () => { }); it("leaves no bare NOW() / CURRENT_TIMESTAMP outside the allowlist", () => { - expect(offenders()).toEqual([]); + const reads = bareReads(); + const inFile = (rel: string) => + reads.filter((r) => r.startsWith(`${rel}:`)).length; + const over = reads.filter((r) => { + const rel = r.slice(0, r.indexOf(":")); + return !ALLOWED[rel] || inFile(rel) > ALLOWED[rel].hits; + }); + expect(over).toEqual([]); }); - it("keeps the allowlist honest: every entry still has a bare read", () => { - for (const rel of Object.keys(ALLOWED)) { - const lines = readFileSync(join(ROOT, rel), "utf8") - .replace(ANCHORED, "") - .split("\n"); - expect( - lines.some((line) => SQL_CLOCK.test(line)), - rel, - ).toBe(true); + it("keeps the allowlist honest: every entry holds exactly its count", () => { + const reads = bareReads(); + for (const [rel, { hits }] of Object.entries(ALLOWED)) { + expect(reads.filter((r) => r.startsWith(`${rel}:`)).length, rel).toBe( + hits, + ); } }); }); diff --git a/src/__tests__/timezone-explicit-formatter-guard.test.ts b/src/__tests__/timezone-explicit-formatter-guard.test.ts index be1b66a56..7b4977f68 100644 --- a/src/__tests__/timezone-explicit-formatter-guard.test.ts +++ b/src/__tests__/timezone-explicit-formatter-guard.test.ts @@ -31,19 +31,24 @@ const REPO_ROOT = resolve(__dirname, "../.."); /** * Bare formatter sites that are correct as they stand. Keep this short: it is - * an inventory of exceptions, not a parking space. Every entry says why. + * an inventory of exceptions, not a parking space. Every entry says why, and + * how many bare sites the file holds: an exception covers those sites, not + * whatever lands in the same file next. */ -const ALLOWLIST: ReadonlyArray<{ file: string; why: string }> = [ +const ALLOWLIST: ReadonlyArray<{ file: string; hits: number; why: string }> = [ { file: "src/components/ui/calendar.tsx", + hits: 1, why: "A `data-day` attribute on a calendar cell, derived from the grid's own arithmetic. It identifies a cell, it does not render a recorded moment.", }, { file: "src/components/cycle/cycle-calendar.tsx", + hits: 2, why: "Weekday headers walked from a fixed local date, and a month label built from a month anchor. Both are calendar arithmetic with no instant behind them.", }, { file: "src/lib/intl/formatter-cache.ts", + hits: 2, why: "The shared cache itself. Its options come from the caller, so the timezone is the caller's to supply and the guard should be asking them, not this.", }, ]; @@ -171,9 +176,13 @@ describe("date formatters name the calendar they render in", () => { }); it("has no unexplained formatter without an explicit timeZone", () => { - const unexplained = hits.filter( - (h) => !ALLOWLIST.some((a) => a.file === h.file), - ); + const unexplained = hits.filter((h) => { + const entry = ALLOWLIST.find((a) => a.file === h.file); + return ( + !entry || + hits.filter((other) => other.file === h.file).length > entry.hits + ); + }); if (unexplained.length > 0) { throw new Error( `${unexplained.length} formatter call(s) render a date without naming a timezone:\n\n` + @@ -182,7 +191,7 @@ describe("date formatters name the calendar they render in", () => { "profile timezone (`readStoredTimezone() || DEFAULT_TIMEZONE` on the client, " + "`resolveUserTimezone(userId)` on the server), or use the shared helpers in " + "@/lib/format. If the value is calendar arithmetic rather than a recorded " + - "instant, add the file to ALLOWLIST in this test with the reason.", + "instant, add the file to ALLOWLIST in this test with the reason and its count.", ); } }); @@ -209,9 +218,9 @@ describe("date formatters name the calendar they render in", () => { // that has moved on, and it quietly covers whatever lands in that file next. for (const entry of ALLOWLIST) { expect( - hits.some((h) => h.file === entry.file), - `stale allowlist entry — ${entry.file} no longer has a bare formatter`, - ).toBe(true); + hits.filter((h) => h.file === entry.file).length, + `stale allowlist entry — ${entry.file} holds a different number of bare formatters`, + ).toBe(entry.hits); } }); });