Document what EXTERNAL does to a contested day, and pin it

Review caught a behaviour change this branch causes but never wrote down.
`pickCanonicalSourceRows` takes the first ladder source present in a day's
bucket and filters the day to that source. Before, a typed reading and a
bridged one were both MANUAL, so both survived. Now MANUAL ranks, EXTERNAL
does not, and the bridged row leaves the canonical set on any day that also
has a typed one.

That reaches every surface needing one source per day — the daily totals
behind the charts, the health score, the doctor report, correlations, mood
crosstabs and personal-record detection. A day with only bridged readings is
untouched: nothing ranked is present, so the picker's pass-through keeps
everything.

The changelog says so in the user's terms, including the part that is a fix
rather than a change: two indistinguishable MANUAL rows were both counted,
which inflated a running total on a day somebody did both.

Four cases pin it, because it is emergent from a rank ladder and stated in
no single place: the contested day, the bridged-only day, the both-MANUAL
shape the change moves away from, and a stored ladder that names EXTERNAL
being honoured. Ranking EXTERNAL turns the first two red, which is the
review anybody adding it to a ladder should have to pass.
This commit is contained in:
Antiheld86 authored and Marc-André Bombeck committed 2026-09-25 06:55:17 +02:00
1 parent 9fc1413181
commit a099d813ee
1 file changed
+109
@@ -515,3 +515,112 @@ describe("pickCanonicalSourceRows — v1.4.25 W8c two-axis device-type picker",
expect(out.canonicalRows[0].source).toBe("WITHINGS");
});
});
/**
* v1.38.x — what the EXTERNAL source does to a contested day.
*
* `EXTERNAL` is on no priority ladder, deliberately: the `steps` and `spo2`
* defaults already sit at `metricSourceLadder`'s `.max(8)`, so a ninth entry
* would come back from `GET /api/auth/me/source-priority`, be PUT back whole
* by the settings page, and 422 — breaking ladder saves for everyone.
*
* That omission is not inert. It decides a real behaviour, and one that
* CHANGED when ingested rows stopped being labelled `MANUAL`: before, a typed
* reading and a bridged one were the same source and both survived a day; now
* `MANUAL` ranks, `EXTERNAL` does not, and the bridged row leaves the
* canonical set for that day. Seven call sites consume this picker (daily
* totals, the health score, the doctor report, correlations, mood crosstabs,
* PR detection), so the effect is user-visible and is written up in the
* changelog.
*
* Pinned here because it is emergent from a rank ladder rather than stated
* anywhere in code: put `EXTERNAL` on a ladder and the first case below goes
* red, which is the review anybody adding it deserves.
*/
describe("pickCanonicalSourceRows — the unranked EXTERNAL source", () => {
const day = "2026-09-02";
function weightRow(
source: "MANUAL" | "EXTERNAL",
value: number,
hour: string,
) {
return {
measuredAt: new Date(`${day}T${hour}:00Z`),
source,
deviceType: null,
type: "WEIGHT" as const,
value,
};
}
it("keeps the typed reading and drops the bridged one on a contested day", () => {
// The behaviour change itself. `MANUAL` is on the weight ladder and
// `EXTERNAL` is not, so the day resolves to the reading the person
// entered — a hand correction still wins where one number is needed.
const out = pickCanonicalSourceRows(
[
weightRow("EXTERNAL", 82.4, "06:30"),
weightRow("MANUAL", 81.9, "08:00"),
],
"weight",
null,
isoDayKey,
);
expect(out.canonicalRows).toHaveLength(1);
expect(out.canonicalRows[0].source).toBe("MANUAL");
expect(out.canonicalRows[0].value).toBe(81.9);
expect(out.pickedByDay.get(day)).toBe("MANUAL");
});
it("keeps a bridged-only day, since nothing on the ladder is present", () => {
// The `!picked` pass-through. Without this arm an account that ONLY ever
// pushes readings would go dark on every surface listed above, which
// would be a far worse regression than the one above is a change.
const out = pickCanonicalSourceRows(
[
weightRow("EXTERNAL", 82.4, "06:30"),
weightRow("EXTERNAL", 82.1, "20:00"),
],
"weight",
null,
isoDayKey,
);
expect(out.canonicalRows).toHaveLength(2);
expect(out.canonicalRows.every((r) => r.source === "EXTERNAL")).toBe(true);
// No ladder source won, so the day names no pick.
expect(out.pickedByDay.has(day)).toBe(false);
});
it("kept both when the bridged row was still labelled MANUAL", () => {
// The pre-change shape, asserted so the diff between old and new
// behaviour lives in the suite rather than only in the changelog. Two
// `MANUAL` rows are indistinguishable to the picker and both survive —
// which for a cumulative metric is the double-count the new labelling
// removes.
const out = pickCanonicalSourceRows(
[weightRow("MANUAL", 82.4, "06:30"), weightRow("MANUAL", 81.9, "08:00")],
"weight",
null,
isoDayKey,
);
expect(out.canonicalRows).toHaveLength(2);
});
it("lets a user who ranks EXTERNAL themselves have it win", () => {
// Not reachable from the settings UI today (the ladder cap keeps it off
// the page), but the picker resolves whatever ladder it is handed, and a
// stored ladder naming it must be honoured rather than ignored.
const out = pickCanonicalSourceRows(
[
weightRow("EXTERNAL", 82.4, "06:30"),
weightRow("MANUAL", 81.9, "08:00"),
],
"weight",
{ metricPriority: { weight: ["EXTERNAL", "MANUAL"] } },
isoDayKey,
);
expect(out.canonicalRows).toHaveLength(1);
expect(out.canonicalRows[0].source).toBe("EXTERNAL");
});
});