Pin what decides the score's composition

The module-to-pillar mapping had no test of its own. It lived in
ternaries, so the only way to notice it had changed was for someone's
score to change.

The new file drives the real reader against a fake database and pins
three things. The composition for every module combination with no
stored recipe, compared against the old expression written out verbatim,
which is what makes it a before-and-after rather than a restatement. The
difference between a pillar that is selected and has no data yet, which
keeps its row, and a pillar whose module is off, which has no row at
all. And the breadth refusal at read time, whose reason has to be the
shared rule's verdict on the set the reader resolved, so a second copy
of that rule would show up as a disagreement.
This commit is contained in:
Marc-André Bombeck committed 2026-07-31 15:51:26 +02:00
1 parent 76e68f0247
commit 0a877bdf4d
1 file changed
+506
@@ -0,0 +1,506 @@
/**
* v1.35.0 — what decides the Health Score's composition, pinned.
*
* The module switches used to decide it alone, from a stack of ternaries
* inside the reader that nothing could see and nothing tested. This file
* is the regression test the capability audit found missing, written for
* the NEW semantics: the stored recipe says what counts, the modules say
* what there can be data for, and the score takes the intersection.
*
* Three promises live here.
*
* 1. **Nobody's number moves on upgrade.** Every module combination with
* a never-written recipe resolves to exactly the set the ternaries
* built. The old expression is reproduced verbatim below as
* `compositionBeforeTheSwap`, so this is a real before-and-after
* comparison rather than a restatement of the new code.
*
* 2. **A module that is off removes the pillar entirely.** Not a score,
* not a "waiting for data" line, no row at all. That line belongs to
* a pillar someone selected and could still record today; a module
* they switched off is not waiting for anything.
*
* 3. **One breadth rule, applied twice.** The settings write refuses a
* selection that cannot produce a score; the reader refuses to
* produce one from a set that fails the same rule. The block at the
* bottom drives the REAL reader and asserts its refusal reason is
* `evaluateScoreBreadth`'s verdict on the reader's own resolved set,
* so a second copy of the rule that disagreed would show up here.
*
* The reader runs against a fake Prisma rather than hand-built pillar
* inputs on purpose: the composition is assembled halfway down that
* function, and a test that skips it would prove the wrong end.
*/
import { describe, expect, it } from "vitest";
import type {
GlucoseContext,
MeasurementSource,
MeasurementType,
PrismaClient,
SleepStage,
} from "@/generated/prisma/client";
import { MODULE_KEYS } from "@/lib/modules/registry";
import { evaluateScoreBreadth } from "../breadth";
import { healthScoreConfigFromSelection } from "../config";
import {
MODULE_GATED_PILLARS,
MODULE_SCORE_PILLARS,
pillarsWithModuleData,
type ScoreModuleKey,
type ScoreReaderModules,
} from "../modules";
import { computeUserHealthScore } from "../reader";
import { SCORE_PILLAR_IDS, type ScorePillarId } from "../types";
const NOW = new Date("2026-08-20T12:00:00.000Z");
const DAY_MS = 86_400_000;
/**
* The `availablePillars` expression exactly as it stood before the swap,
* kept here as the "before" side of the inheritance proof. It must never
* be edited to follow the implementation: the day it disagrees with the
* reader is the day somebody's number moved.
*/
function compositionBeforeTheSwap(
modules: ScoreReaderModules,
): ScorePillarId[] {
return [
"BLOOD_PRESSURE",
...(modules.glucose || modules.labs ? (["GLYCAEMIA"] as const) : []),
"ACTIVITY",
...(modules.sleep ? (["SLEEP"] as const) : []),
"ADIPOSITY",
...(modules.mentalHealth ? (["WELLBEING"] as const) : []),
"FITNESS",
...(modules.labs ? (["LIPIDS"] as const) : []),
];
}
/** All sixteen on/off states of the four modules the reader is handed. */
const MODULE_COMBINATIONS: ScoreReaderModules[] = [0, 1].flatMap((glucose) =>
[0, 1].flatMap((labs) =>
[0, 1].flatMap((sleep) =>
[0, 1].map((mentalHealth) => ({
glucose: glucose === 1,
labs: labs === 1,
sleep: sleep === 1,
mentalHealth: mentalHealth === 1,
})),
),
),
);
const ALL_MODULES_ON: ScoreReaderModules = {
glucose: true,
labs: true,
sleep: true,
mentalHealth: true,
};
function describeModules(modules: ScoreReaderModules): string {
const off = (Object.keys(modules) as ScoreModuleKey[]).filter(
(key) => !modules[key],
);
return off.length === 0 ? "every module on" : `${off.join(" + ")} off`;
}
type MeasurementRow = {
type: MeasurementType;
value: number;
unit: string;
source: MeasurementSource;
measuredAt: Date;
deviceType: string | null;
glucoseContext: GlucoseContext | null;
sleepStage: SleepStage | null;
};
type AssessmentRow = {
instrument: "PHQ9" | "GAD7" | "WHO5" | "SCI";
totalScore: number;
item9Flagged: boolean;
takenAt: Date;
};
function measurement(
type: MeasurementType,
value: number,
measuredAt: Date,
extra: Partial<MeasurementRow> = {},
): MeasurementRow {
return {
type,
value,
unit: "",
source: "MANUAL",
measuredAt,
deviceType: null,
glucoseContext: null,
sleepStage: null,
...extra,
};
}
/**
* An account with enough data for four pillars to score: steps, sleep,
* a waist-to-height ratio and a WHO-5. Blood pressure, glycaemia, lipids
* and fitness stay honestly absent, which is what lets the same fixture
* show a selected-but-dataless pillar beside a module-withdrawn one.
*/
const MEASUREMENTS: MeasurementRow[] = [
...Array.from({ length: 45 }, (_, index) =>
measurement(
"ACTIVITY_STEPS",
9_000,
new Date(NOW.getTime() - (44 - index) * DAY_MS),
),
),
...Array.from({ length: 45 }, (_, index) =>
measurement(
"SLEEP_DURATION",
450,
new Date(
new Date(NOW.getTime() - (44 - index) * DAY_MS).setUTCHours(6, 0, 0, 0),
),
{ sleepStage: "ASLEEP" },
),
),
measurement("WAIST_TO_HEIGHT", 0.45, new Date(NOW.getTime() - 20 * DAY_MS)),
];
const ASSESSMENTS: AssessmentRow[] = [
{
instrument: "WHO5",
totalScore: 80,
item9Flagged: false,
takenAt: new Date(NOW.getTime() - 20 * DAY_MS),
},
];
function fakePrisma(): PrismaClient {
return {
measurement: {
findMany: async (query: {
where: {
type: { in: MeasurementType[] };
measuredAt: { gte: Date };
glucoseContext?: GlucoseContext;
};
}) =>
MEASUREMENTS.filter(
(row) =>
query.where.type.in.includes(row.type) &&
row.measuredAt >= query.where.measuredAt.gte &&
(query.where.glucoseContext === undefined ||
row.glucoseContext === query.where.glucoseContext),
),
},
mentalHealthAssessment: { findMany: async () => ASSESSMENTS },
labResult: { findMany: async () => [] },
dismissedPriorityItem: { findUnique: async () => null },
} as unknown as PrismaClient;
}
function score(args: {
modules: ScoreReaderModules;
healthScoreConfigJson: unknown;
}) {
return computeUserHealthScore({
prisma: fakePrisma(),
userId: "user-1",
now: NOW,
profile: {
dateOfBirth: null,
gender: null,
heightCm: null,
timezone: "UTC",
sourcePriorityJson: null,
thresholdsJson: null,
},
modules: args.modules,
healthScoreConfigJson: args.healthScoreConfigJson,
bpTargets: null,
bpEnvelope: null,
bpEnvelopePriorWeek: null,
bpEnvelopePriorTwoWeeks: null,
});
}
/** A stored recipe, built through the same boundary the write route uses. */
function recipe(selection: readonly ScorePillarId[]): unknown {
return healthScoreConfigFromSelection({
selection,
version: 1,
changedAt: new Date("2026-08-01T00:00:00.000Z"),
});
}
describe("the module-to-pillar map", () => {
it("reproduces the ternaries it replaced, for every module combination", () => {
for (const modules of MODULE_COMBINATIONS) {
expect(
pillarsWithModuleData(modules),
`availability for ${describeModules(modules)}`,
).toEqual(compositionBeforeTheSwap(modules));
}
});
it("keeps glycaemia fed by either glucose or labs alone", () => {
const both = { ...ALL_MODULES_ON, glucose: false, labs: false };
expect(pillarsWithModuleData({ ...both, glucose: true })).toContain(
"GLYCAEMIA",
);
expect(pillarsWithModuleData({ ...both, labs: true })).toContain(
"GLYCAEMIA",
);
expect(pillarsWithModuleData(both)).not.toContain("GLYCAEMIA");
});
it("drops lipids with labs but never with glucose", () => {
expect(
pillarsWithModuleData({ ...ALL_MODULES_ON, labs: false }),
).not.toContain("LIPIDS");
expect(
pillarsWithModuleData({ ...ALL_MODULES_ON, glucose: false }),
).toContain("LIPIDS");
});
it("leaves the four core-domain pillars ungated", () => {
// These read weight, blood pressure, steps and waist — domains with
// no module switch at all, so no module can withdraw their data.
// Pinned rather than derived so an addition to the map that started
// gating one of them has to be a deliberate edit here too.
const ungated = SCORE_PILLAR_IDS.filter(
(id) => !MODULE_GATED_PILLARS.has(id),
);
expect(ungated).toEqual([
"BLOOD_PRESSURE",
"ACTIVITY",
"ADIPOSITY",
"FITNESS",
]);
const allOff: ScoreReaderModules = {
glucose: false,
labs: false,
sleep: false,
mentalHealth: false,
};
expect(pillarsWithModuleData(allOff)).toEqual(ungated);
});
it("names only real modules and real pillars", () => {
for (const key of Object.keys(MODULE_SCORE_PILLARS) as ScoreModuleKey[]) {
expect(MODULE_KEYS as readonly string[]).toContain(key);
for (const id of MODULE_SCORE_PILLARS[key]) {
expect(SCORE_PILLAR_IDS as readonly string[]).toContain(id);
}
}
});
});
describe("inheritance: a never-written recipe keeps the number it had", () => {
it("resolves the pre-swap composition for every module combination", async () => {
for (const modules of MODULE_COMBINATIONS) {
const report = await score({ modules, healthScoreConfigJson: null });
expect(
report.pillars.map((pillar) => pillar.id),
`composition for ${describeModules(modules)}`,
).toEqual(compositionBeforeTheSwap(modules));
}
});
it("gives the sleep-module-off account the same composition and the same number as before", async () => {
const modules = { ...ALL_MODULES_ON, sleep: false };
const report = await score({ modules, healthScoreConfigJson: null });
expect(report.pillars.map((pillar) => pillar.id)).toEqual([
"BLOOD_PRESSURE",
"GLYCAEMIA",
"ACTIVITY",
"ADIPOSITY",
"WELLBEING",
"FITNESS",
"LIPIDS",
]);
expect(report.composite.status).toBe("ok");
if (report.composite.status !== "ok") return;
expect(report.composite.value.composition).toEqual([
"ACTIVITY",
"ADIPOSITY",
"WELLBEING",
]);
// Steps 9,000/day against the 10,000 plateau scores 90, a
// waist-to-height of 0.45 scores 100, a WHO-5 of 80 scores 80, and
// the mean is 90. The number itself is pinned, because "the
// composition is right" and "the number did not move" are two
// different claims and the second one is the promise.
expect(report.composite.value.score).toBe(90);
});
it("cannot be re-included by a recipe that selects everything", async () => {
const modules = { ...ALL_MODULES_ON, sleep: false };
const inherited = await score({ modules, healthScoreConfigJson: null });
const authored = await score({
modules,
healthScoreConfigJson: recipe([...SCORE_PILLAR_IDS]),
});
expect(authored.pillars.map((pillar) => pillar.id)).toEqual(
inherited.pillars.map((pillar) => pillar.id),
);
expect(authored.composite.status).toBe("ok");
if (authored.composite.status !== "ok") return;
if (inherited.composite.status !== "ok") return;
expect(authored.composite.value.score).toBe(
inherited.composite.value.score,
);
});
});
describe("a module and a recipe answer different questions", () => {
it("changes what is read, not what is composed, when a module is toggled", async () => {
const selection = [...SCORE_PILLAR_IDS];
const configJson = recipe(selection);
const on = await score({
modules: ALL_MODULES_ON,
healthScoreConfigJson: configJson,
});
const off = await score({
modules: { ...ALL_MODULES_ON, sleep: false },
healthScoreConfigJson: configJson,
});
// The authored recipe is the same blob either way — the toggle wrote
// nothing to it. What changed is whether sleep is being recorded.
expect(on.pillars.map((pillar) => pillar.id)).toContain("SLEEP");
expect(off.pillars.map((pillar) => pillar.id)).not.toContain("SLEEP");
expect(off.pillars.map((pillar) => pillar.id)).toEqual(
on.pillars.map((pillar) => pillar.id).filter((id) => id !== "SLEEP"),
);
// Sleep really is scoring in this fixture, so the toggle is moving a
// pillar that counts rather than one that was absent anyway. Without
// this the two assertions above would pass on an empty account.
expect(
on.pillars.find((pillar) => pillar.id === "SLEEP")?.result.status,
).toBe("ok");
if (on.composite.status !== "ok" || off.composite.status !== "ok") {
throw new Error("both windows should score");
}
expect(on.composite.value.composition).toContain("SLEEP");
expect(off.composite.value.composition).not.toContain("SLEEP");
});
it("drops a deselected pillar even while its module keeps recording", async () => {
const report = await score({
modules: ALL_MODULES_ON,
healthScoreConfigJson: recipe(
SCORE_PILLAR_IDS.filter((id) => id !== "SLEEP"),
),
});
expect(report.pillars.map((pillar) => pillar.id)).not.toContain("SLEEP");
expect(report.composite.status).toBe("ok");
if (report.composite.status !== "ok") return;
expect(report.composite.value.composition).not.toContain("SLEEP");
});
});
describe("selected and waiting for data versus switched off", () => {
it("keeps a selected pillar with no data as an honestly absent row", async () => {
// Lipids is selected, labs is on, and there is no lab result. That is
// the "selected, waiting for data" state: the row exists, it just has
// nothing to score yet, and recording a lipid panel would fill it.
const report = await score({
modules: ALL_MODULES_ON,
healthScoreConfigJson: recipe([...SCORE_PILLAR_IDS]),
});
const lipids = report.pillars.find((pillar) => pillar.id === "LIPIDS");
expect(lipids).toBeDefined();
expect(lipids?.result.status).toBe("insufficient");
});
it("shows nothing at all for a pillar whose module is off", async () => {
// The decision this test exists to pin: a module the person switched
// off is not "waiting" for anything. Telling them their score wants
// sleep data they chose to stop recording would be nagging in the
// voice of their own settings, and with a never-written recipe the
// word "selected" would be describing a default they never picked.
const report = await score({
modules: { ...ALL_MODULES_ON, sleep: false },
healthScoreConfigJson: recipe([...SCORE_PILLAR_IDS]),
});
expect(report.pillars.some((pillar) => pillar.id === "SLEEP")).toBe(false);
if (report.composite.status !== "ok") return;
expect(report.composite.value.composition).not.toContain("SLEEP");
});
it("never renders a switched-off module's pillar as a failed read", async () => {
// Absence and failure carry different affordances. A module that is
// off is neither: it produces no row, so it can never be mistaken
// for a read that broke.
const report = await score({
modules: { ...ALL_MODULES_ON, mentalHealth: false },
healthScoreConfigJson: null,
});
expect(report.pillars.some((pillar) => pillar.id === "WELLBEING")).toBe(
false,
);
});
});
describe("the breadth rule still refuses at read time", () => {
const NARROW: Array<{ what: string; selection: ScorePillarId[] }> = [
{
what: "a recipe that leaves two scoring domains",
selection: ["ACTIVITY", "ADIPOSITY", "BLOOD_PRESSURE"],
},
{
what: "a recipe with nothing physiological that scores",
selection: ["ACTIVITY", "WELLBEING"],
},
{
what: "a recipe of pillars that have no data at all",
selection: ["BLOOD_PRESSURE", "GLYCAEMIA", "LIPIDS"],
},
];
for (const { what, selection } of NARROW) {
it(`refuses ${what}, with the reason the shared rule gives`, async () => {
const report = await score({
modules: ALL_MODULES_ON,
healthScoreConfigJson: recipe(selection),
});
const scoring = report.pillars
.filter((pillar) => pillar.result.status === "ok")
.map((pillar) => pillar.id);
const verdict = evaluateScoreBreadth(scoring);
expect(verdict.ok).toBe(false);
expect(report.composite.status).toBe("insufficient");
if (report.composite.status !== "insufficient") return;
expect(report.composite.reason).toBe(verdict.reason);
});
}
it("still produces a score when the recipe clears the rule", async () => {
const report = await score({
modules: ALL_MODULES_ON,
healthScoreConfigJson: recipe([
"ACTIVITY",
"ADIPOSITY",
"WELLBEING",
"SLEEP",
]),
});
expect(report.composite.status).toBe("ok");
});
});