Files
HealthLog/tests/integration/health-status-implausible-values.test.ts
T
Marc-André Bombeck 4563bce5b3 fix(insights): keep impossible readings out of the personal band
The baseline-drift card reported a pulse in the hundreds of millions against a
usual range in the tens of thousands. Neither figure is a heart rate, and both
came from the same place.

The application declares a plausibility domain per metric, and every
interactive, import, MCP and Telegram write validates against it. The
derivation layer did not ask. So a single stored value outside that domain — a
provider unit-decode slip, or a row from a writer that never had the gate — was
folded into the day mean, carried by the median into the band, and then held
against itself as "currently". The two numbers in the sentence were wrong by
different amounts, which is why the line read as nonsense rather than as an
exaggeration.

Both readers now refuse a value the application has already declared
impossible: the per-day means behind the band, and the latest-day mean the
deviation reads. A day left with no plausible reading contributes nothing
rather than a fabricated point, and the vital drops out of the comparison — the
coverage floor already knows how to report an absent day. The rollup branch
applies the same rule to a bucket mean, since the rollup writer averages
whatever is stored. Everything downstream of `readDayMeanSeries` inherits it,
which is every consumer of the usual range: the band strip, the drift card, the
changepoint detector, readiness, trajectory, the coach read.

The card also printed raw doubles. Each figure now renders at the metric's own
decimal precision through the shared formatter, so a mean reads as a number
rather than as a float and follows the reader's number convention.

Left open, and recorded for a separate pass: the provider sync writers do not
enforce the domain on the way in, so the impossible rows keep landing.
2026-08-07 14:16:17 +02:00

161 lines
5.0 KiB
TypeScript

/**
* The Insights baseline-drift read, driven as a route against real Postgres.
*
* The unit suite pins the arithmetic; this pins the pipe. A stored pulse value
* the application declares impossible used to reach the card twice over — once
* as "currently", once through the band it inflated — and the line read
* "your pulse is above your usual range, currently 111,287,531.01 instead of
* the usual 36,016.75". The shipped `GET` export answers here, so the readers
* really have to apply the domain rather than a test's copy of it.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => {
cookieJar.set(name, value);
},
delete: (name: string) => {
cookieJar.delete(name);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
const DAY_MS = 24 * 60 * 60 * 1000;
/** The two numbers off the report, kept verbatim so the case stays legible. */
const IMPOSSIBLE_PULSE = 111287531.01;
/** A raised but real pulse, so the card has a genuine deviation to report. */
const REAL_RAISED_PULSE = 112;
let counter = 0;
async function makeUser() {
const suffix = `hs-${counter++}`;
return getPrismaClient().user.create({
data: {
username: `hs-${suffix}`,
email: `hs-${suffix}@example.test`,
role: "USER",
timezone: "Europe/Berlin",
},
});
}
async function signIn(userId: string) {
const session = await getPrismaClient().session.create({
data: { userId, expiresAt: new Date(Date.now() + 60_000) },
});
cookieJar.set("healthlog_session", session.id);
}
/** A fortnight of ordinary readings for one metric, one per day. */
async function seedDaily(
userId: string,
type: "PULSE" | "WEIGHT",
valueFor: (dayIndex: number) => number,
) {
const rows = [];
for (let d = 14; d >= 1; d -= 1) {
rows.push({
userId,
type,
value: valueFor(d),
unit: type === "PULSE" ? "bpm" : "kg",
measuredAt: new Date(Date.now() - d * DAY_MS),
source: "MANUAL" as const,
});
}
await getPrismaClient().measurement.createMany({ data: rows });
}
async function readHealthStatus() {
const { GET } = await import("@/app/api/insights/health-status/route");
const response = await (GET as (request: NextRequest) => Promise<Response>)(
new NextRequest("http://localhost/api/insights/health-status", {
method: "GET",
}),
);
expect(response.status).toBe(200);
const body = (await response.json()) as {
data: {
present: boolean;
deviations: {
type: string;
value: number;
low: number;
high: number;
}[];
};
};
return body.data;
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
});
describe("GET /api/insights/health-status — implausible stored values", () => {
it("reports today's real pulse, not the impossible row beside it", async () => {
const user = await makeUser();
await signIn(user.id);
// Two banded vitals — the coincident engine needs a pair before it reports.
await seedDaily(user.id, "PULSE", (d) => 64 + (d % 5));
await seedDaily(user.id, "WEIGHT", (d) => 80 + (d % 3) * 0.2);
// Today: a genuinely raised pulse — so the card has something real to say —
// and beside it one impossible reading, the shape a provider unit-decode
// slip leaves behind.
await getPrismaClient().measurement.createMany({
data: [
{
userId: user.id,
type: "PULSE",
value: REAL_RAISED_PULSE,
unit: "bpm",
measuredAt: new Date(Date.now() - 120_000),
source: "MANUAL",
},
{
userId: user.id,
type: "PULSE",
value: IMPOSSIBLE_PULSE,
unit: "bpm",
measuredAt: new Date(Date.now() - 60_000),
source: "MANUAL",
},
],
});
const data = await readHealthStatus();
const pulse = data.deviations.find((d) => d.type === "PULSE");
// The deviation has to be there — otherwise this asserts nothing.
expect(pulse).toBeDefined();
expect(pulse!.value).toBeCloseTo(REAL_RAISED_PULSE, 6);
expect(pulse!.high).toBeLessThan(300);
expect(pulse!.low).toBeGreaterThanOrEqual(20);
// And the impossible number itself never reaches the wire.
expect(JSON.stringify(data)).not.toContain(String(IMPOSSIBLE_PULSE));
});
});