From 1915f915265ee0c476ce7fc6241f248194a995f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 05:58:45 +0200 Subject: [PATCH] test(insights): pin the clock in the two-service workout collapse case The case built its sessions from the real clock: one copy a day before now, the other 30 s later. The cross-source workout picker groups sessions into fixed five-minute slots of the epoch, so whenever the suite ran in the last 30 s of a slot the two copies landed in different slots and counted twice. It had nothing to do with day boundaries or time zones: the outcome followed the minute of the wall clock alone. The case now pins Date to an instant well inside a slot. With the pin at 12:04:45Z or 23:59:45Z the case failed under UTC, Europe/Berlin and Pacific/Kiritimati alike; at the committed 12:02:00Z it passes under all three. The production code reads no clock here (the block takes `now` from its caller). What the flake exposed is the fixed slot grid itself: two copies of one session that straddle a slot edge are not collapsed. That is a change to the picker every workout surface shares, so it is left for a change of its own. --- src/lib/insights/__tests__/features.test.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/lib/insights/__tests__/features.test.ts b/src/lib/insights/__tests__/features.test.ts index f38e096f0..98dc39deb 100644 --- a/src/lib/insights/__tests__/features.test.ts +++ b/src/lib/insights/__tests__/features.test.ts @@ -559,6 +559,14 @@ describe("extractFeatures — v1.22 workouts block", () => { }); it("counts a session synced by two services once", async () => { + // The cross-source picker groups sessions into fixed five-minute slots + // of the epoch (`pick-canonical-workout-rows.ts`). With a start taken + // from the real clock, the 30 s gap between the two copies crossed a + // slot edge whenever the test ran in the last 30 s of a slot, and the + // pair counted twice. The clock is pinned to an instant well inside a + // slot, so the case tests the collapse rather than the time of day. + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(Date.parse("2026-10-22T12:02:00Z")); const startedAt = new Date(Date.now() - 1 * DAY_MS); prismaMock.workout.findMany.mockResolvedValue([ { @@ -576,10 +584,14 @@ describe("extractFeatures — v1.22 workouts block", () => { totalDistanceM: 5010, }, ]); - const f = await extractFeatures("user-1", false); - expect(f.workouts?.last7.count).toBe(1); - expect(f.workouts?.last7.totalDurationMin).toBe(30); - expect(f.workouts?.last7.totalDistanceKm).toBe(5); + try { + const f = await extractFeatures("user-1", false); + expect(f.workouts?.last7.count).toBe(1); + expect(f.workouts?.last7.totalDurationMin).toBe(30); + expect(f.workouts?.last7.totalDistanceKm).toBe(5); + } finally { + vi.useRealTimers(); + } }); });