fix(insights): read the mood fallback over the rollup's window

While the mood rollup tier is still empty the feature block falls back to
raw entries, and that fallback read one year where the rollup read five,
so the same block described a different history until the tier caught up.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 09:48:30 +02:00
1 parent 5fd4333017
commit 071d23537b
2 files changed
+25 -8

No files matched your search

@@ -703,3 +703,22 @@ describe("extractFeatures — blood-pressure spread", () => {
expect(features.bloodPressure?.sdSys30).toBe(8.2);
});
});
describe("extractFeatures — mood fallback window", () => {
it("reads the raw fallback over the rollup's five years, not one", async () => {
// Empty rollup tier: the raw fallback answers.
prismaMock.moodEntryRollup.findMany.mockResolvedValue([]);
await extractFeatures("user-mood-window", false, {
only: new Set(["mood"]),
});
const call = prismaMock.moodEntry.findMany.mock.calls.find(
([args]) =>
(args as { where?: { moodLoggedAt?: unknown } })?.where?.moodLoggedAt,
);
expect(call).toBeDefined();
const since = (call![0] as { where: { moodLoggedAt: { gte: Date } } }).where
.moodLoggedAt.gte;
const years = (Date.now() - since.getTime()) / (365 * dayMs);
expect(years).toBeGreaterThan(4.9);
});
});
+6 -8
View File
@@ -1021,7 +1021,7 @@ async function readMoodFeature(
//
// Coverage-fallback: when raw entries exist but the rollup tier is
// still empty (legacy account before boot-time backfill caught up),
// we fall back to a bounded 1-year raw walk and fire the warm-up so
// we fall back to a raw walk over the same window and fire the warm-up so
// the next request lands on the rollup tier. Same posture as
// `/api/mood/analytics`.
void ensureUserMoodRollupsFresh(userId);
@@ -1062,15 +1062,13 @@ async function readMoodFeature(
});
moodLatestScore = latestEntry?.score ?? null;
} else {
// Coverage-fallback. Bounded 1-year walk (instead of the legacy
// unbounded `findMany`) so even a cache miss is capped.
const oneYearAgo = laterDate(
new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
historyFloor,
);
// Coverage-fallback over the same window the rollup read covers
// (`moodSince`, five years). A one-year fallback answered the same
// question with a different history while the tier was warming up;
// mood entries are a few a day, so five years stays a bounded read.
const moodEntriesRaw = await prisma.moodEntry.findMany({
// v1.7.0 sync — exclude tombstoned rows.
where: { userId, deletedAt: null, moodLoggedAt: { gte: oneYearAgo } },
where: { userId, deletedAt: null, moodLoggedAt: { gte: moodSince } },
orderBy: { moodLoggedAt: "asc" },
select: { score: true, moodLoggedAt: true, date: true },
});