test(workouts): pin enriched list and detail envelopes

This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:56:21 +02:00
1 parent bcef9cf51d
commit ed63a3bf32
2 files changed
+133

No files matched your search

@@ -83,6 +83,7 @@ interface FakeWorkoutRow {
avgHeartRate: number | null;
maxHeartRate: number | null;
totalEnergyKcal: number | null;
metadata: Record<string, unknown> | null;
createdAt: Date;
}
@@ -105,6 +106,7 @@ function makeRow(
avgHeartRate: 145,
maxHeartRate: 170,
totalEnergyKcal: 320,
metadata: null,
createdAt: start,
};
}
@@ -147,6 +149,40 @@ describe("GET /api/workouts — canonical dedup", () => {
expect(body.data.meta.droppedDuplicates).toBe(1);
});
it("backfills missing Apple HR aggregates from its WHOOP twin", async () => {
const apple = {
...makeRow("w-apple", "APPLE_HEALTH", "2026-05-15T07:00:00Z"),
avgHeartRate: null,
maxHeartRate: null,
metadata: { hkVersion: "1" },
};
const whoop = {
...makeRow("w-whoop", "WHOOP", "2026-05-15T07:00:30Z"),
avgHeartRate: 148,
maxHeartRate: 181,
metadata: {
zoneDurations: { zone_two_milli: 120_000 },
whoopWorkoutStrain: 12.3,
},
};
vi.mocked(prisma.workout.findMany).mockResolvedValueOnce([
apple,
whoop,
] as never);
const res = await GET(makeRequest());
const body = await res.json();
expect(body.data.workouts).toHaveLength(1);
expect(body.data.workouts[0]).toMatchObject({
id: "w-apple",
source: "APPLE_HEALTH",
avgHr: 148,
maxHr: 181,
});
expect(body.data.meta.droppedDuplicates).toBe(1);
});
it("returns the most recent workouts first", async () => {
// The canonical picker re-sorts its output to startedAt ASC (its rollup
// contract), so the list route must re-establish newest-first AFTER the
@@ -153,6 +153,103 @@ describe("GET /api/workouts/{id}", () => {
expect(body.data.route).toBeNull();
});
it("enriches the Apple winner with only matched WHOOP HR and zones", async () => {
const routeGeometry = {
type: "LineString",
coordinates: [
[11.0, 49.0],
[11.01, 49.01],
],
};
vi.mocked(prisma.workout.findUnique).mockResolvedValueOnce({
...BASE_ROW,
avgHeartRate: null,
maxHeartRate: null,
metadata: { hkVersion: "1", owner: "apple" },
route: {
id: "wr-apple",
geometry: routeGeometry,
sampleTimestamps: null,
createdAt: BASE_ROW.createdAt,
},
} as never);
vi.mocked(prisma.workout.findMany).mockResolvedValueOnce([
{
id: "w-1",
source: "APPLE_HEALTH",
startedAt: BASE_ROW.startedAt,
sportType: "RUNNING",
avgHeartRate: null,
maxHeartRate: null,
metadata: { hkVersion: "1", owner: "apple" },
},
{
id: "w-whoop",
source: "WHOOP",
startedAt: new Date(BASE_ROW.startedAt.getTime() + 30_000),
sportType: "RUNNING",
avgHeartRate: 148,
maxHeartRate: 181,
metadata: {
zoneDurations: {
zone_one_milli: 60_000,
zone_two_milli: 300_000,
},
whoopWorkoutStrain: 12.3,
},
},
] as never);
const res = await GET(makeRequest(), makeParams("w-1"));
const body = await res.json();
expect(body.data).toMatchObject({
id: "w-1",
source: "APPLE_HEALTH",
canonicalId: "w-1",
avgHr: 148,
maxHr: 181,
route: { geometry: routeGeometry },
metadata: { hkVersion: "1", owner: "apple" },
});
expect(body.data.metadata).not.toHaveProperty("whoopWorkoutStrain");
expect(body.data.metadata).not.toHaveProperty("zoneDurations");
expect(body.data.zones.model).toBe("whoop");
expect(body.data.hrSeries).toBeNull();
});
it("does not overwrite the canonical winner's existing HR values", async () => {
vi.mocked(prisma.workout.findUnique).mockResolvedValueOnce(
BASE_ROW as never,
);
vi.mocked(prisma.workout.findMany).mockResolvedValueOnce([
{
id: "w-1",
source: "APPLE_HEALTH",
startedAt: BASE_ROW.startedAt,
sportType: "RUNNING",
avgHeartRate: BASE_ROW.avgHeartRate,
maxHeartRate: BASE_ROW.maxHeartRate,
metadata: BASE_ROW.metadata,
},
{
id: "w-whoop",
source: "WHOOP",
startedAt: new Date(BASE_ROW.startedAt.getTime() + 30_000),
sportType: "RUNNING",
avgHeartRate: 155,
maxHeartRate: 190,
metadata: null,
},
] as never);
const res = await GET(makeRequest(), makeParams("w-1"));
const body = await res.json();
expect(body.data.avgHr).toBe(145);
expect(body.data.maxHr).toBe(170);
});
it("returns 401 without a session", async () => {
vi.mocked(getSession).mockResolvedValueOnce(null);
const res = await GET(makeRequest(), makeParams("w-1"));