BUG A — timed events written 4h off: EventForm sent a naive local wall-clock string with no offset; the UTC API container parsed it via new Date() as UTC, so 09:00 America/Toronto serialized to DTSTART:...090000Z. Fix: new apps/pwa/src/lib/eventDateTime.ts serializes timed events to an unambiguous UTC instant in the browser (where the operator's zone is known); all-day stays a DATE string. No backend change. BUG B — created events attached to the wrong user's calendar + duplicate calendar rows per poll: calendars had no unique key on url, and poller/sync matched calendars by url alone — so under the shared single Fastmail account (D-16) one member's collection resolved to the other member's row. Fix: composite unique(user_id, url); scope poller lookup + sync select to (userId, url); hand migration 0001 (dedup + add key), applied to the live DB. Regression tests fail against the buggy url-only predicate. API 98/98, PWA 140/140, tsc clean both packages.
54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
/**
|
|
* BUG A regression — write-path timezone serialization.
|
|
*
|
|
* Verifies that timed events are serialized to an unambiguous UTC instant
|
|
* (so the operator's wall-clock time round-trips correctly regardless of the
|
|
* API container's timezone), while all-day events keep their DATE strings.
|
|
*
|
|
* The PWA vitest harness runs with TZ=UTC, so the assertions are computed
|
|
* relative to the local zone (whatever it is) rather than hard-coding an offset.
|
|
* The core guarantee under test: the serialized timed value is a UTC instant
|
|
* (ends in 'Z') derived from the LOCAL wall clock — never the naive wall-clock
|
|
* string passed through verbatim, and never an instant that loses the local hour.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import { serializeEventDateTime, localWallClockToUtcIso } from './eventDateTime.js'
|
|
|
|
describe('serializeEventDateTime (BUG A — write-path TZ)', () => {
|
|
it('serializes a timed start to a UTC instant (ends in Z)', () => {
|
|
const { start } = serializeEventDateTime(false, '2026-06-07', '09:00', '2026-06-07', '10:00')
|
|
expect(start.endsWith('Z')).toBe(true)
|
|
// It must NOT be the naive wall-clock string (the original bug shape).
|
|
expect(start).not.toBe('2026-06-07T09:00:00')
|
|
})
|
|
|
|
it('the serialized instant round-trips back to the SAME local wall clock', () => {
|
|
// This is the heart of BUG A: 09:00 in → 09:00 back out in the operator's zone.
|
|
const { start, end } = serializeEventDateTime(false, '2026-06-07', '09:00', '2026-06-07', '10:30')
|
|
const startBack = new Date(start)
|
|
expect(startBack.getHours()).toBe(9)
|
|
expect(startBack.getMinutes()).toBe(0)
|
|
const endBack = new Date(end)
|
|
expect(endBack.getHours()).toBe(10)
|
|
expect(endBack.getMinutes()).toBe(30)
|
|
})
|
|
|
|
it('equals the instant new Date(local parts) produces — not a passthrough', () => {
|
|
const { start } = serializeEventDateTime(false, '2026-06-07', '09:00', '2026-06-07', '10:00')
|
|
expect(start).toBe(new Date('2026-06-07T09:00:00').toISOString())
|
|
})
|
|
|
|
it('leaves all-day events as DATE strings (no time, no Z) — D-13 contract', () => {
|
|
const { start, end } = serializeEventDateTime(true, '2026-06-07', '09:00', '2026-06-09', '10:00')
|
|
expect(start).toBe('2026-06-07')
|
|
expect(end).toBe('2026-06-09')
|
|
})
|
|
|
|
it('localWallClockToUtcIso round-trips a local wall clock to a UTC instant', () => {
|
|
const iso = localWallClockToUtcIso('2026-06-07', '09:00')
|
|
expect(iso.endsWith('Z')).toBe(true)
|
|
expect(new Date(iso).getHours()).toBe(9)
|
|
})
|
|
})
|