diff --git a/backend/tests/pdf-preview-runtime.unit.test.ts b/backend/tests/pdf-preview-runtime.unit.test.ts index 2a993680..adb971fc 100644 --- a/backend/tests/pdf-preview-runtime.unit.test.ts +++ b/backend/tests/pdf-preview-runtime.unit.test.ts @@ -179,7 +179,17 @@ test("the bundled PDF.js version decodes and paints an actual PDF page with iden const rootRequire = createRequire( new URL("../../package.json", import.meta.url), ); - const { createCanvas } = rootRequire("@napi-rs/canvas"); + // Paint the way imports/pdf.ts reads: PDF.js's bundled standard fonts, not + // whatever fonts the runner happens to have. Without a Path2D, PDF.js cannot + // draw those font outlines on a Node canvas. + const { createCanvas, Path2D } = rootRequire("@napi-rs/canvas"); + const standardFontDataUrl = new URL( + "../../node_modules/pdfjs-dist/standard_fonts/", + import.meta.url, + ).pathname; + const globals = globalThis as { Path2D?: unknown }; + const previousPath2D = globals.Path2D; + globals.Path2D = Path2D; const { getDocument } = await import("../../node_modules/pdfjs-dist/legacy/build/pdf.mjs"); const stream = "BT /F1 20 Tf 20 100 Td (Live Note preview) Tj ET"; @@ -204,7 +214,8 @@ test("the bundled PDF.js version decodes and paints an actual PDF page with iden const task = getDocument({ data: new TextEncoder().encode(source), isEvalSupported: false, - useSystemFonts: true, + useSystemFonts: false, + standardFontDataUrl, useWasm: false, }); try { @@ -237,5 +248,6 @@ test("the bundled PDF.js version decodes and paints an actual PDF page with iden ); } finally { await task.destroy(); + globals.Path2D = previousPath2D; } }); diff --git a/backend/tests/saved-views.test.ts b/backend/tests/saved-views.test.ts index fda9faae..59730589 100644 --- a/backend/tests/saved-views.test.ts +++ b/backend/tests/saved-views.test.ts @@ -944,9 +944,56 @@ test("SQL date narrowing agrees with core across DST, source zones, spans and ti ]; try { await db.query("BEGIN ISOLATION LEVEL REPEATABLE READ"); + // Postgres and Node each ship their own tz database, and a rule change + // (such as Cuba keeping UTC-4 in November 2026) reaches them at different + // times. Where the two disagree on a zone's offset near an anchor, the + // comparison would test the tz databases rather than our SQL, so skip it. + const wallClock = (zone: string) => + new Intl.DateTimeFormat("en-CA", { + timeZone: zone, + hourCycle: "h23", + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + const drifted = new Set(); + for (const zone of sourceZones) { + const format = wallClock(zone); + for (const day of anchors) { + const instants = Array.from( + { length: 14 * 4 }, + (_, step) => + new Date( + Date.parse(`${addDays(day, -3)}T00:00:00Z`) + + step * 6 * 3_600_000, + ), + ); + const { rows } = await db.query<{ local: string }>( + `SELECT to_char(t AT TIME ZONE $2, 'YYYY-MM-DD HH24:MI') AS local + FROM unnest($1::timestamptz[]) WITH ORDINALITY AS u(t, n) ORDER BY n`, + [instants.map((instant) => instant.toISOString()), zone], + ); + const node = instants.map((instant) => { + const part = Object.fromEntries( + format.formatToParts(instant).map((p) => [p.type, p.value]), + ); + return `${part.year}-${part.month}-${part.day} ${part.hour}:${part.minute}`; + }); + if (rows.some((row, index) => row.local !== node[index])) + drifted.add(`${zone} ${day}`); + } + } + if (drifted.size) + console.warn( + `date parity: skipped where tz databases disagree: ${[...drifted].join(", ")}`, + ); + assert.ok(drifted.size <= 2, [...drifted].join(", ")); let serial = 0; for (const zone of sourceZones) for (const day of anchors) { + if (drifted.has(`${zone} ${day}`)) continue; for (const offset of [-1, 0, 1]) { const date = addDays(day, offset), start = dayTime(date, 0, zone).toISOString(); @@ -968,6 +1015,7 @@ test("SQL date narrowing agrees with core across DST, source zones, spans and ti } for (const timeZone of sourceZones) for (const anchor of anchors) { + if (drifted.has(`${timeZone} ${anchor}`)) continue; const now = new Date(`${anchor}T12:00:00.000Z`), today = localDateKey(now, timeZone); const common = { text: marker, status: "any" as const };