mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user