test(mood-rollups): pin DST fall-back Berlin midnight slip

QA Specialist-H1 (v1.4.39): the wave-report noon-anchored parity tests
miss the DST-boundary case where the UTC-anchored rollup day-key and the
user's local wall-clock day-key diverge. Pinned with a route-parity test
that logs at `2025-10-25T23:30:00Z` (Berlin local `2025-10-26` 00:30,
post fall-back) and asserts the rollup tier emits `2025-10-25` —
documents the UTC-anchor trade-off the route's header comment now spells
out. v1.5 per-user-tz bucketing closes the gap; until then this is the
contract.
This commit is contained in:
Marc-André Bombeck committed 2026-05-18 12:53:43 +02:00
1 parent 3c1a50cbb2
commit ff4e5faf5f
2 files changed
+52

No files matched your search

@@ -216,6 +216,48 @@ describe("GET /api/mood/analytics", () => {
expect(body.data.summary.latest).toBe(5);
});
it("pins the DST fall-back Berlin midnight slip — UTC bucket vs local day diverge by one calendar day", async () => {
// QA Specialist-H1 (v1.4.39): 2025-10-25T23:30:00Z is 00:30 local
// in Europe/Berlin on 2025-10-26 (one hour AFTER the fall-back
// transition). The mood rollup writer anchors `bucketStart` on
// UTC midnight (mirroring the measurement-rollup convention), so
// a write logged at this instant materialises a rollup row keyed
// on `2025-10-25T00:00:00Z` — the UTC-anchored day — even though
// the user's local wall clock reads 2025-10-26. The route's
// `utcDayLabel` emits the same day-key the rollup row carries.
//
// This pins the documented behaviour. The legacy live-fallback
// path uses `MoodEntry.date` (TZ-anchored) and therefore emits
// `2025-10-26` for the same instant — a slip we document in the
// route's header comment as the cost of the cache-tier semantics.
// The v1.5 per-user-tz bucketing (P7 in the v1.4.38 perf audit)
// closes the gap by anchoring the rollup table on the same day-
// key the legacy path uses. Until then this is the contract.
vi.mocked(prisma.moodEntryRollup.findMany).mockResolvedValue([
{
userId: "user-mood-1",
granularity: "DAY",
bucketStart: new Date("2025-10-25T00:00:00.000Z"),
count: 1,
mean: 4,
minScore: 4,
maxScore: 4,
sd: null,
computedAt: new Date(),
},
] as never);
const res = await callGet();
expect(res.status).toBe(200);
const body = (await res.json()) as MoodAnalyticsBody;
// The rollup tier pins the entry on the UTC-anchored day-key,
// NOT the user's local 2025-10-26 — same convention as the
// measurement-rollup tier.
expect(body.data.entries).toEqual([
{ date: "2025-10-25", score: 4, samples: 1 },
]);
});
it("serves the cached envelope on a warm read without touching the DB", async () => {
// First read populates the cache.
vi.mocked(prisma.moodEntryRollup.findMany).mockResolvedValue([
+10
View File
@@ -56,6 +56,16 @@ interface MoodAnalyticsResult {
* (Berlin year-round) the two labels agree on every entry whose
* timestamp doesn't straddle the UTC boundary — i.e. every realistic
* mood log, which a human submits during waking hours.
*
* Trade-off (QA Specialist-H1, v1.4.39): on DST fall-back nights the
* UTC anchor and the user's local wall-clock day-key can diverge by
* one calendar day. Example: `2025-10-25T23:30:00Z` is 00:30 local
* in Europe/Berlin on `2025-10-26` (one hour after the fall-back
* transition); the rollup row is keyed on `2025-10-25` (UTC) while
* the legacy live-fallback path would emit `2025-10-26`. This is
* pinned by the route-parity DST test. v1.5 per-user-tz bucketing
* (audit P7) anchors the rollup on the same day-key the legacy path
* uses, closing the gap.
*/
function utcDayLabel(d: Date): string {
const yyyy = d.getUTCFullYear();