mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
The dashboard snapshot and the /api/mood/analytics route each carried a hand-copied rollup read, coverage fallback, and daily summarize of the same numbers. Two reads of one metric is the seam where the dashboard tile and the insights mood sparkline can drift apart. Extract buildMoodDailySeries as the single mood engine and route both callers through it, so the same mood number reads identically on the dashboard, the insights surface, and the iOS client.
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
/**
|
|
* v1.17.1 — the single mood engine `buildMoodDailySeries`.
|
|
*
|
|
* Pins the one-engine contract: the dashboard snapshot (`buildMoodBlock`)
|
|
* and the `/api/mood/analytics` route both read through this function, so
|
|
* the same number must read identically on dashboard + insights. Covers
|
|
* the rollup fast-path and the legacy live fallback, and asserts the live
|
|
* fallback collapses multi-entry days into the daily mean exactly once.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@/lib/db", () => ({
|
|
prisma: {
|
|
moodEntry: { findMany: vi.fn() },
|
|
moodEntryRollup: { findMany: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/lib/rollups/mood-rollups", async () => {
|
|
const actual = await vi.importActual<
|
|
typeof import("@/lib/rollups/mood-rollups")
|
|
>("@/lib/rollups/mood-rollups");
|
|
return {
|
|
...actual,
|
|
ensureUserMoodRollupsFresh: vi
|
|
.fn()
|
|
.mockResolvedValue({ recomputed: false }),
|
|
readMoodDayRollups: vi.fn(),
|
|
};
|
|
});
|
|
|
|
import { buildMoodDailySeries } from "../mood-series";
|
|
import { prisma } from "@/lib/db";
|
|
import { readMoodDayRollups } from "@/lib/rollups/mood-rollups";
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
|
|
vi.mocked(readMoodDayRollups).mockResolvedValue([] as never);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("buildMoodDailySeries", () => {
|
|
it("reads the rollup tier and skips the raw walk when DAY rows exist", async () => {
|
|
vi.mocked(readMoodDayRollups).mockResolvedValue([
|
|
{ bucketStart: new Date("2026-06-01T00:00:00Z"), mean: 4, count: 2 },
|
|
{ bucketStart: new Date("2026-06-02T00:00:00Z"), mean: 3, count: 1 },
|
|
] as never);
|
|
|
|
const series = await buildMoodDailySeries("user-1");
|
|
|
|
expect(series.source).toBe("rollup");
|
|
expect(prisma.moodEntry.findMany).not.toHaveBeenCalled();
|
|
expect(series.entries).toEqual([
|
|
{ date: "2026-06-01", score: 4, samples: 2 },
|
|
{ date: "2026-06-02", score: 3, samples: 1 },
|
|
]);
|
|
// entryCount sums the per-day samples (2 + 1).
|
|
expect(series.entryCount).toBe(3);
|
|
expect(series.summary.count).toBe(2);
|
|
});
|
|
|
|
it("falls back to the live walk once and collapses multi-entry days to the daily mean", async () => {
|
|
vi.mocked(readMoodDayRollups).mockResolvedValue([] as never);
|
|
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([
|
|
{ date: "2026-06-01", score: 5 },
|
|
{ date: "2026-06-01", score: 3 },
|
|
{ date: "2026-06-02", score: 4 },
|
|
] as never);
|
|
|
|
const series = await buildMoodDailySeries("user-1");
|
|
|
|
expect(series.source).toBe("live");
|
|
// Two distinct calendar days; day one is the mean of its two entries.
|
|
expect(series.entries).toEqual([
|
|
{ date: "2026-06-01", score: 4, samples: 2 },
|
|
{ date: "2026-06-02", score: 4, samples: 1 },
|
|
]);
|
|
expect(series.entryCount).toBe(3);
|
|
// summarize sees one DataPoint per day, not per raw entry.
|
|
expect(series.summary.count).toBe(2);
|
|
});
|
|
|
|
it("returns an empty series for a user with no rollups and no raw entries", async () => {
|
|
const series = await buildMoodDailySeries("user-1");
|
|
expect(series.entries).toEqual([]);
|
|
expect(series.entryCount).toBe(0);
|
|
// An empty table reads as the rollup tier for annotation parity.
|
|
expect(series.source).toBe("rollup");
|
|
expect(series.summary.count).toBe(0);
|
|
});
|
|
});
|