mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
test(workouts): pin enriched list and detail envelopes
This commit is contained in:
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"));
|
||||
|
||||
Reference in new issue
Block a user