Files
HealthLog/src/lib/validations/__tests__/source-priority.test.ts
T
Marc-André Bombeck c456a236a7 fix(analytics): rank Health Connect and the hand-entered sources on the ladders
A source a ladder does not name ranks 90, below every source it does.
Five had no place anywhere: a weight typed as a reply to a Telegram
reminder lost its day to a WHOOP strap's estimate, one an assistant
logged over MCP lost to a Fitbit estimate, and a Health Connect day
lost to a hand-typed count.

Health Connect now sits right after Apple Health on the ladders for
what it imports; EXTERNAL, MCP and TELEGRAM sit right after MANUAL, in
the alphabetical order they already broke ties in, so MANUAL still
outranks the person's own bridge. Relative to each other the sources
the ladders already named keep their order, and saved ladders gain
the new entries at their end as before.
2026-10-21 13:07:34 +02:00

617 lines
21 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";
import { measurementSourceEnum } from "../measurement";
/**
* v1.4.25 — per-user, per-metric-class source-priority foundation.
* v1.4.25 — 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);
});
// The cap is the number of sources: every source may be ranked once (a
// ladder that ranks all sixteen is valid), and one entry more is refused.
it("accepts a ladder that ranks every source, and rejects one entry more", () => {
const every = [...measurementSourceEnum.options];
expect(every.length).toBeGreaterThan(8);
expect(sourcePrioritySchema.safeParse({ weight: every }).success).toBe(
true,
);
expect(
sourcePrioritySchema.safeParse({ weight: [...every, "MANUAL"] }).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"]);
// The schema caps a ladder at one entry per source.
expect(ladder.length).toBeLessThanOrEqual(
measurementSourceEnum.options.length,
);
});
});
describe("DEFAULT_SOURCE_PRIORITY — the sources that had no place (v1.43.11)", () => {
const HAND_ENTERED = ["EXTERNAL", "MCP", "TELEGRAM"] as const;
// What Health Connect imports, by ladder (`import/health-connect/mapper.ts`).
const HEALTH_CONNECT_LADDERS = [
"steps",
"activeEnergy",
"walkingRunningDistance",
"bloodPressure",
"bodyFat",
"bodyTemperature",
"hrvRmssd",
"spo2",
"respiratoryRate",
"restingHeartRate",
"vo2Max",
"weight",
] as const;
it("ranks the hand-entered sources right after MANUAL wherever MANUAL ranks", () => {
for (const [key, ladder] of Object.entries(DEFAULT_SOURCE_PRIORITY)) {
const i = (ladder as string[]).indexOf("MANUAL");
if (i === -1) continue;
expect(ladder.slice(i + 1, i + 1 + HAND_ENTERED.length), key).toEqual([
...HAND_ENTERED,
]);
}
});
it("ranks Health Connect right after Apple Health on every ladder it imports for", () => {
for (const key of HEALTH_CONNECT_LADDERS) {
const ladder = DEFAULT_SOURCE_PRIORITY[key] as string[];
expect(ladder[ladder.indexOf("APPLE_HEALTH") + 1], key).toBe(
"HEALTH_CONNECT",
);
}
});
it("leaves the order of the sources the ladders already named alone", () => {
const added = new Set<string>([...HAND_ENTERED, "HEALTH_CONNECT"]);
const without = (key: keyof typeof DEFAULT_SOURCE_PRIORITY) =>
DEFAULT_SOURCE_PRIORITY[key].filter((s) => !added.has(s));
expect(without("weight")).toEqual([
"WITHINGS",
"APPLE_HEALTH",
"MANUAL",
"WHOOP",
"FITBIT",
"GOOGLE_HEALTH",
]);
expect(without("bloodPressure")).toEqual([
"WITHINGS",
"APPLE_HEALTH",
"MANUAL",
]);
expect(without("spo2")).toEqual([
"WITHINGS",
"WHOOP",
"FITBIT",
"GOOGLE_HEALTH",
"OURA",
"POLAR",
"APPLE_HEALTH",
"MANUAL",
]);
});
it("fits every default ladder within the schema cap", () => {
for (const [key, ladder] of Object.entries(DEFAULT_SOURCE_PRIORITY)) {
expect(ladder.length, key).toBeLessThanOrEqual(
measurementSourceEnum.options.length,
);
}
});
});