fix(workouts): sanitize WHOOP zone enrichment

This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:58:24 +02:00
1 parent 89fefce7ac
commit d2f6bf33eb
2 files changed
+68 -3

No files matched your search

@@ -89,7 +89,11 @@ describe("pickCanonicalWorkoutRows", () => {
avgHeartRate: 146,
maxHeartRate: 181,
metadata: {
zoneDurations: { zone_one_milli: 60_000 },
zoneDurations: {
zone_one_milli: 60_000,
nestedSecret: { mustNotFlow: true },
zone_six_milli: 90_000,
},
whoopWorkoutStrain: 12.3,
},
};
@@ -107,6 +111,11 @@ describe("pickCanonicalWorkoutRows", () => {
zoneDurations: { zone_one_milli: 60_000 },
},
});
expect(
(picked.metadata as Record<string, unknown>).zoneDurations,
).toEqual({
zone_one_milli: 60_000,
});
expect(picked.metadata).not.toHaveProperty("whoopWorkoutStrain");
expect(apple).toEqual({
id: "apple",
@@ -149,6 +158,32 @@ describe("pickCanonicalWorkoutRows", () => {
});
});
it("rejects malformed WHOOP zone durations instead of partially copying them", () => {
const result = pickCanonicalWorkoutRows<RowFixture>([
{
id: "apple",
startedAt: new Date("2026-06-03T06:30:00Z"),
sportType: "running",
source: "APPLE_HEALTH",
metadata: { owner: "apple" },
},
{
id: "whoop",
startedAt: new Date("2026-06-03T06:30:30Z"),
sportType: "running",
source: "WHOOP",
metadata: {
zoneDurations: {
zone_one_milli: 60_000,
zone_two_milli: -1,
},
},
},
]);
expect(result[0].metadata).toEqual({ owner: "apple" });
});
it("does not enrich across sport or fixed-window boundaries", () => {
const apple: RowFixture = {
id: "apple",
@@ -77,13 +77,43 @@ function jsonRecord(value: unknown): Record<string, unknown> | null {
: null;
}
const WHOOP_ZONE_DURATION_KEYS = [
"zone_zero_milli",
"zone_one_milli",
"zone_two_milli",
"zone_three_milli",
"zone_four_milli",
"zone_five_milli",
] as const;
function sanitizedWhoopZoneDurations(
metadata: unknown,
): Record<string, number> | null {
const raw = jsonRecord(jsonRecord(metadata)?.zoneDurations);
if (!raw) return null;
const sanitized: Record<string, number> = {};
for (const key of WHOOP_ZONE_DURATION_KEYS) {
const value = raw[key];
if (value === undefined) continue;
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
value < 0
) {
return null;
}
sanitized[key] = value;
}
return Object.keys(sanitized).length > 0 ? sanitized : null;
}
function enrichFromWhoopTwin<T extends WorkoutPickerRow>(
winner: T,
donor: T,
): T {
const winnerMetadata = jsonRecord(winner.metadata);
const donorMetadata = jsonRecord(donor.metadata);
const donorZones = donorMetadata?.zoneDurations;
const donorZones = sanitizedWhoopZoneDurations(donor.metadata);
const needsZones =
donorZones != null && winnerMetadata?.zoneDurations == null;
const needsAvg =