Files
HealthLog/src/lib/validations/__tests__/source-priority.test.ts
T
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00

542 lines
18 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
DEFAULT_DEVICE_TYPE_PRIORITY,
DEFAULT_SOURCE_PRIORITY,
SOURCE_PRIORITY_METRIC_KEYS,
deviceTypeEnum,
getDeviceTypeLadder,
getSourceLadder,
normalizeDeviceType,
parseSourcePriority,
sourcePrioritySchema,
} from "../source-priority";
/**
* v1.4.25 W5e — per-user, per-metric-class source-priority foundation.
* v1.4.25 W8c — two-axis extension: `metricPriority` + `deviceTypePriority`.
*/
describe("sourcePrioritySchema", () => {
it("accepts an empty object (every key is optional)", () => {
const result = sourcePrioritySchema.safeParse({});
expect(result.success).toBe(true);
});
it("accepts a partial flat shape (one metric class only) — W5e compat", () => {
const result = sourcePrioritySchema.safeParse({
weight: ["WITHINGS", "MANUAL"],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.weight).toEqual(["WITHINGS", "MANUAL"]);
}
});
it("accepts the W8c nested `metricPriority` shape", () => {
const result = sourcePrioritySchema.safeParse({
metricPriority: {
weight: ["MANUAL", "WITHINGS"],
steps: ["WITHINGS", "APPLE_HEALTH"],
},
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.metricPriority?.weight).toEqual([
"MANUAL",
"WITHINGS",
]);
expect(result.data.metricPriority?.steps).toEqual([
"WITHINGS",
"APPLE_HEALTH",
]);
}
});
it("accepts the W8c `deviceTypePriority` global default", () => {
const result = sourcePrioritySchema.safeParse({
deviceTypePriority: {
default: ["watch", "phone", "ring"],
},
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.deviceTypePriority?.default).toEqual([
"watch",
"phone",
"ring",
]);
}
});
it("accepts the W8c `deviceTypePriority` per-metric override", () => {
const result = sourcePrioritySchema.safeParse({
deviceTypePriority: {
default: ["watch", "ring", "phone"],
ACTIVITY_STEPS: ["phone", "watch"],
WEIGHT: ["scale"],
},
});
expect(result.success).toBe(true);
if (result.success) {
// The catchall path stores per-metric overrides under their
// MeasurementType-enum key.
expect(result.data.deviceTypePriority?.ACTIVITY_STEPS).toEqual([
"phone",
"watch",
]);
expect(result.data.deviceTypePriority?.WEIGHT).toEqual(["scale"]);
}
});
it("accepts both axes in the same payload", () => {
const result = sourcePrioritySchema.safeParse({
metricPriority: {
weight: ["MANUAL", "WITHINGS"],
},
deviceTypePriority: {
default: ["watch", "ring"],
},
});
expect(result.success).toBe(true);
});
it("rejects an unknown source string", () => {
const result = sourcePrioritySchema.safeParse({
weight: ["GARMIN", "WITHINGS"],
});
expect(result.success).toBe(false);
});
it("rejects an unknown device-type string", () => {
const result = sourcePrioritySchema.safeParse({
deviceTypePriority: {
default: ["fitbit-tracker"],
},
});
expect(result.success).toBe(false);
});
it("rejects more than 8 sources in a single metric class", () => {
const result = sourcePrioritySchema.safeParse({
weight: [
"WITHINGS",
"APPLE_HEALTH",
"MANUAL",
"IMPORT",
"WITHINGS",
"APPLE_HEALTH",
"MANUAL",
"IMPORT",
"WITHINGS",
],
});
expect(result.success).toBe(false);
});
it("rejects a non-array value for a metric class", () => {
const result = sourcePrioritySchema.safeParse({
weight: "WITHINGS",
});
expect(result.success).toBe(false);
});
});
describe("deviceTypeEnum", () => {
it("accepts every documented device-type slot", () => {
for (const value of [
"watch",
"band",
"ring",
"phone",
"scale",
"other",
"unknown",
] as const) {
expect(deviceTypeEnum.safeParse(value).success).toBe(true);
}
});
});
describe("DEFAULT_SOURCE_PRIORITY", () => {
it("covers every metric key declared in SOURCE_PRIORITY_METRIC_KEYS", () => {
// A new metric class added to the constant list without a default
// would silently read as `undefined` at the call site and crash
// the aggregator — this guard makes that mismatch a test failure.
for (const key of SOURCE_PRIORITY_METRIC_KEYS) {
expect(
DEFAULT_SOURCE_PRIORITY[key],
`missing default for ${key}`,
).toBeInstanceOf(Array);
expect(DEFAULT_SOURCE_PRIORITY[key].length).toBeGreaterThan(0);
}
});
it("places WITHINGS first for point measurements (the maintainer directive)", () => {
// Withings devices are the primary sensor for point readings; the
// canonical row should come from the scale / cuff / ScanWatch /
// Thermo rather than HealthKit's second-hand mirror.
for (const key of [
"weight",
"bloodPressure",
"pulse",
"bodyFat",
"bodyTemperature",
"spo2",
"vo2Max",
] as const) {
expect(DEFAULT_SOURCE_PRIORITY[key][0]).toBe("WITHINGS");
}
});
it("places APPLE_HEALTH first for cumulative metrics", () => {
// HealthKit aggregates ScanWatch + iPhone sensors into a single
// canonical stream and has higher resolution than Withings' nightly
// summary for the cumulative activity metrics.
for (const key of [
"steps",
"activeEnergy",
"walkingRunningDistance",
"flightsClimbed",
] as const) {
expect(DEFAULT_SOURCE_PRIORITY[key][0]).toBe("APPLE_HEALTH");
}
});
it("places WHOOP first for the recovery-input ladders (v1.11)", () => {
// v1.11.0 — a worn-all-night WHOOP strap has higher-resolution
// overnight sampling than the iPhone-relayed HealthKit summary or the
// Withings nightly summary for sleep / HRV / RHR / respiratory rate;
// it leads those ladders ahead of APPLE_HEALTH.
for (const key of [
"sleep",
"hrv",
"restingHeartRate",
"respiratoryRate",
] as const) {
expect(DEFAULT_SOURCE_PRIORITY[key][0]).toBe("WHOOP");
}
});
it("ranks device-native recovery above the COMPUTED proxy (v1.11/v1.17)", () => {
// v1.11.0 — native-vs-derived: a device-native Recovery outranks
// HealthLog's COMPUTED proxy when both exist, with the proxy as the
// fallback for users without a wearable. v1.17.0 — Oura readiness and
// Polar nightly recovery slot between WHOOP and the proxy.
expect(DEFAULT_SOURCE_PRIORITY.recovery).toEqual([
"WHOOP",
"OURA",
"POLAR",
"COMPUTED",
]);
// The COMPUTED proxy stays the last resort regardless of how many
// native wearables precede it.
expect(DEFAULT_SOURCE_PRIORITY.recovery.at(-1)).toBe("COMPUTED");
});
it("slots Polar + Oura below the established wearables (v1.17)", () => {
// v1.17.0 — Polar / Oura are worn wearables in the same overnight class
// as WHOOP/Fitbit; they rank below the established straps but above the
// iPhone-relayed HealthKit summary and the Withings nightly summary on
// the recovery-input ladders.
for (const key of ["sleep", "hrv", "restingHeartRate"] as const) {
const ladder = DEFAULT_SOURCE_PRIORITY[key];
expect(ladder).toContain("OURA");
expect(ladder).toContain("POLAR");
expect(ladder.indexOf("OURA")).toBeLessThan(
ladder.indexOf("APPLE_HEALTH"),
);
expect(ladder.indexOf("POLAR")).toBeLessThan(
ladder.indexOf("APPLE_HEALTH"),
);
expect(ladder.indexOf("WHOOP")).toBeLessThan(ladder.indexOf("OURA"));
}
});
it("appends Oura + Polar to the activity ladders below Apple Health (v1.17)", () => {
// v1.17.0 — Oura + Polar write ACTIVITY_STEPS + ACTIVE_ENERGY_BURNED;
// they belong on the activity ladders as legitimate sources, ranked
// below the phone-aggregated APPLE_HEALTH but above a MANUAL entry, so
// an Oura/Polar-only day is not dropped when another source coexists.
for (const key of [
"steps",
"activeEnergy",
"walkingRunningDistance",
] as const) {
const ladder = DEFAULT_SOURCE_PRIORITY[key];
expect(ladder).toContain("OURA");
expect(ladder).toContain("POLAR");
// Below the phone-aggregated all-day Apple Health stream.
expect(ladder.indexOf("APPLE_HEALTH")).toBeLessThan(
ladder.indexOf("OURA"),
);
expect(ladder.indexOf("APPLE_HEALTH")).toBeLessThan(
ladder.indexOf("POLAR"),
);
// Above a hand-typed MANUAL entry — a real device beats nothing.
expect(ladder.indexOf("OURA")).toBeLessThan(ladder.indexOf("MANUAL"));
expect(ladder.indexOf("POLAR")).toBeLessThan(ladder.indexOf("MANUAL"));
// No existing source loses its rank: Apple Health still leads.
expect(ladder[0]).toBe("APPLE_HEALTH");
}
});
it("ranks the pulse wearables above a MANUAL reading (v1.17)", () => {
// v1.17.0 — a copy-paste slip had Polar / Oura ranked BELOW a hand-typed
// MANUAL pulse. Continuous optical-HR wearables must sit above MANUAL,
// matching the device-source order on the rhr / hrv ladders.
const ladder = DEFAULT_SOURCE_PRIORITY.pulse;
for (const device of ["FITBIT", "OURA", "POLAR"] as const) {
expect(ladder.indexOf(device)).toBeLessThan(ladder.indexOf("MANUAL"));
}
// Device-source order matches the rhr / hrv ladders (FITBIT > OURA > POLAR).
expect(ladder.indexOf("FITBIT")).toBeLessThan(ladder.indexOf("OURA"));
expect(ladder.indexOf("OURA")).toBeLessThan(ladder.indexOf("POLAR"));
// Withings stays the primary point-measurement source.
expect(ladder[0]).toBe("WITHINGS");
});
});
describe("DEFAULT_DEVICE_TYPE_PRIORITY", () => {
it("is non-empty and contains every enum slot", () => {
// The picker treats an unknown device-type as `"unknown"` — to
// stay safe the constant must include the `unknown` slot or
// legacy/NULL-tagged rows would never resolve a rank.
expect(DEFAULT_DEVICE_TYPE_PRIORITY.length).toBe(7);
expect(DEFAULT_DEVICE_TYPE_PRIORITY).toContain("unknown");
});
it("places `watch` first (open-wearables-compatible default)", () => {
expect(DEFAULT_DEVICE_TYPE_PRIORITY[0]).toBe("watch");
});
});
describe("parseSourcePriority", () => {
it("returns defaults for null input", () => {
const out = parseSourcePriority(null);
expect(out).toMatchObject(DEFAULT_SOURCE_PRIORITY);
expect(out.metricPriority).toEqual(DEFAULT_SOURCE_PRIORITY);
expect(out.deviceTypePriority).toEqual({});
});
it("returns defaults for undefined input", () => {
const out = parseSourcePriority(undefined);
expect(out).toMatchObject(DEFAULT_SOURCE_PRIORITY);
});
it("returns defaults for malformed input (forward-compat fallback)", () => {
expect(parseSourcePriority({ weight: "WITHINGS" })).toMatchObject(
DEFAULT_SOURCE_PRIORITY,
);
expect(parseSourcePriority("not an object")).toMatchObject(
DEFAULT_SOURCE_PRIORITY,
);
});
it("merges a flat partial shape onto defaults (W5e compat)", () => {
const partial = {
weight: ["MANUAL", "WITHINGS"] as const,
};
const out = parseSourcePriority(partial);
// v1.16.11 — the stored order leads; default-ladder sources the
// stored array lacks append after it (reconciliation).
expect(out.weight.slice(0, 2)).toEqual(["MANUAL", "WITHINGS"]);
expect(out.metricPriority.weight.slice(0, 2)).toEqual([
"MANUAL",
"WITHINGS",
]);
for (const source of DEFAULT_SOURCE_PRIORITY.weight) {
expect(out.weight).toContain(source);
}
// Other keys keep their defaults.
expect(out.steps).toEqual(DEFAULT_SOURCE_PRIORITY.steps);
expect(out.bloodPressure).toEqual(DEFAULT_SOURCE_PRIORITY.bloodPressure);
});
it("merges a W8c nested `metricPriority` onto defaults", () => {
const partial = {
metricPriority: {
weight: ["MANUAL", "APPLE_HEALTH"] as const,
steps: ["WITHINGS"] as const,
},
};
const out = parseSourcePriority(partial);
expect(out.metricPriority.weight.slice(0, 2)).toEqual([
"MANUAL",
"APPLE_HEALTH",
]);
expect(out.metricPriority.steps[0]).toBe("WITHINGS");
for (const source of DEFAULT_SOURCE_PRIORITY.steps) {
expect(out.metricPriority.steps).toContain(source);
}
// Unchanged metrics keep the constant default.
expect(out.metricPriority.bloodPressure).toEqual(
DEFAULT_SOURCE_PRIORITY.bloodPressure,
);
});
it("prefers W8c nested over W5e flat when both shapes are present", () => {
// A user who edits the new UI ships the nested shape; if a
// legacy/admin tool also stashed the flat shape, the nested wins.
const out = parseSourcePriority({
weight: ["WITHINGS"],
metricPriority: {
weight: ["MANUAL", "WITHINGS"],
},
});
expect(out.weight.slice(0, 2)).toEqual(["MANUAL", "WITHINGS"]);
expect(out.metricPriority.weight.slice(0, 2)).toEqual([
"MANUAL",
"WITHINGS",
]);
});
it("surfaces `deviceTypePriority` round-trip unchanged", () => {
const out = parseSourcePriority({
deviceTypePriority: {
default: ["watch", "ring"],
WEIGHT: ["scale"],
},
});
expect(out.deviceTypePriority.default).toEqual(["watch", "ring"]);
expect(out.deviceTypePriority.WEIGHT).toEqual(["scale"]);
});
it("preserves a fully-specified flat shape (W5e back-compat path)", () => {
const input = {
...DEFAULT_SOURCE_PRIORITY,
weight: ["MANUAL", "APPLE_HEALTH"] as const,
};
const out = parseSourcePriority(input);
expect(out.weight.slice(0, 2)).toEqual(["MANUAL", "APPLE_HEALTH"]);
for (const source of DEFAULT_SOURCE_PRIORITY.weight) {
expect(out.weight).toContain(source);
}
});
});
describe("normalizeDeviceType", () => {
it("returns the canonical value for every enum slot", () => {
for (const value of [
"watch",
"band",
"ring",
"phone",
"scale",
"other",
"unknown",
] as const) {
expect(normalizeDeviceType(value)).toBe(value);
}
});
it("returns `unknown` for null / undefined / unknown strings", () => {
expect(normalizeDeviceType(null)).toBe("unknown");
expect(normalizeDeviceType(undefined)).toBe("unknown");
expect(normalizeDeviceType("fitbit")).toBe("unknown");
expect(normalizeDeviceType(42)).toBe("unknown");
});
});
describe("getSourceLadder + getDeviceTypeLadder helpers", () => {
it("getSourceLadder reads the resolved metric ladder", () => {
const resolved = parseSourcePriority({
metricPriority: { weight: ["MANUAL"] },
});
expect(getSourceLadder(resolved, "weight")[0]).toBe("MANUAL");
expect(getSourceLadder(resolved, "bloodPressure")).toEqual(
DEFAULT_SOURCE_PRIORITY.bloodPressure,
);
});
it("getDeviceTypeLadder prefers a per-metric override", () => {
const resolved = parseSourcePriority({
deviceTypePriority: {
default: ["phone", "watch"],
WEIGHT: ["scale", "watch"],
},
});
expect(getDeviceTypeLadder(resolved, "WEIGHT")).toEqual(["scale", "watch"]);
});
it("getDeviceTypeLadder falls back to the user default ladder", () => {
const resolved = parseSourcePriority({
deviceTypePriority: {
default: ["phone", "watch"],
},
});
expect(getDeviceTypeLadder(resolved, "ACTIVITY_STEPS")).toEqual([
"phone",
"watch",
]);
});
it("getDeviceTypeLadder falls back to the constant default ladder when nothing is set", () => {
const resolved = parseSourcePriority(null);
expect(getDeviceTypeLadder(resolved, "ACTIVITY_STEPS")).toEqual(
DEFAULT_DEVICE_TYPE_PRIORITY,
);
});
});
describe("parseSourcePriority — stored-ladder reconciliation (v1.16.11)", () => {
// A ladder saved before a source existed used to hide that source
// forever: the stored array replaced the default wholesale, the
// settings UI offers reorder but not add, and the picker ranked the
// invisible source last. The resolver now appends default-ladder
// sources missing from the stored ladder AFTER the user's explicit
// order — visible, reorderable, same effective rank as before.
it("appends sources the default ladder gained after the user saved", () => {
// A pre-v1.11 sleep ladder: no WHOOP, no FITBIT.
const resolved = parseSourcePriority({
metricPriority: {
sleep: ["APPLE_HEALTH", "MANUAL", "WITHINGS", "IMPORT"],
},
});
expect(resolved.metricPriority.sleep.slice(0, 4)).toEqual([
"APPLE_HEALTH",
"MANUAL",
"WITHINGS",
"IMPORT",
]);
// Appended in default-ladder order, after the stored entries.
expect(resolved.metricPriority.sleep).toContain("WHOOP");
expect(resolved.metricPriority.sleep).toContain("FITBIT");
expect(resolved.metricPriority.sleep.indexOf("WHOOP")).toBeGreaterThan(3);
expect(resolved.metricPriority.sleep.indexOf("WHOOP")).toBeLessThan(
resolved.metricPriority.sleep.indexOf("FITBIT"),
);
});
it("leaves a complete stored ladder byte-identical", () => {
const full = [...DEFAULT_SOURCE_PRIORITY.sleep].reverse();
const resolved = parseSourcePriority({
metricPriority: { sleep: full },
});
expect(resolved.metricPriority.sleep).toEqual(full);
});
it("keeps stored entries the default ladder does not know", () => {
const resolved = parseSourcePriority({
metricPriority: { sleep: ["MANUAL", "WHOOP"] },
});
expect(resolved.metricPriority.sleep[0]).toBe("MANUAL");
expect(resolved.metricPriority.sleep[1]).toBe("WHOOP");
expect(resolved.metricPriority.sleep).toContain("APPLE_HEALTH");
expect(resolved.metricPriority.sleep).toContain("WITHINGS");
});
});
describe("parseSourcePriority — duplicate tolerance (v1.16.11)", () => {
it("collapses duplicates so a resolved ladder can never overflow the schema cap", () => {
const resolved = parseSourcePriority({
metricPriority: {
weight: ["MANUAL", "MANUAL", "WITHINGS", "MANUAL", "WITHINGS"],
},
});
const ladder = resolved.metricPriority.weight;
expect(new Set(ladder).size).toBe(ladder.length);
expect(ladder.slice(0, 2)).toEqual(["MANUAL", "WITHINGS"]);
expect(ladder.length).toBeLessThanOrEqual(8);
});
});