mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the login, register, token and session paths, mirroring columns every migration under prisma/migrations already creates. The entrypoint applies those migrations before the server listens and stops the boot when one fails, so the mirror never had work to do, and DDL on a request path needs the table's owner: a connection that may only read and write rows fails every login on it. Drop the module, its call sites and the mocks that stubbed it out.
131 lines
5.3 KiB
TypeScript
131 lines
5.3 KiB
TypeScript
/**
|
|
* The demo's short-window charts need a shape to show. Every 90-day series in
|
|
* the seed is a mean-reverting walk, and around a settled target that is flat
|
|
* noise: a 7-day trend line over the last 30 days of blood pressure drew a
|
|
* straight line, which is exactly the view the demo exists to show off. The
|
|
* seed carries a busy stretch a few weeks back that lifts blood pressure to
|
|
* about 144/91 for some ten days, and ramipril goes up on the day it
|
|
* turns.
|
|
*
|
|
* The blood-pressure chart scales one axis to fit systolic and diastolic, so a
|
|
* swing has to be large to read: a first version lifted systolic by 12 mmHg,
|
|
* its 7-day mean swung 8 to 14 mmHg, and on a 60 to 140 axis that still drew a
|
|
* nearly flat line. This runs the real `scripts/seed-demo.ts` and holds the
|
|
* last 30 days to a swing of the size that reads, to day-to-day noise small
|
|
* enough for the 7-day mean to draw an arc, to today's reading back in the
|
|
* settled band, and to the dose change landing on the turn. The seed is random,
|
|
* so the bounds leave room around the measured values (swing about 23 mmHg,
|
|
* day-to-day steps around 1 to 2 mmHg outside the climb and the fall). The
|
|
* peak also has a ceiling: past 148 the chart's padded axis jumps to 50-200.
|
|
*/
|
|
import { beforeAll, describe, expect, it, vi } from "vitest";
|
|
|
|
import { getPrismaClient, truncateAllTables } from "./setup";
|
|
|
|
let systolic: number[];
|
|
let diastolic: number[];
|
|
let means: number[];
|
|
let doseChanges: Array<{ effectiveFrom: Date; doseValue: number }>;
|
|
|
|
beforeAll(async () => {
|
|
const prisma = getPrismaClient();
|
|
await truncateAllTables(prisma);
|
|
const { seed } = await import("../../scripts/seed-demo");
|
|
await seed();
|
|
const user = await prisma.user.findUniqueOrThrow({
|
|
where: { username: "demo" },
|
|
select: { id: true },
|
|
});
|
|
const since = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000);
|
|
const rows = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
type: "BLOOD_PRESSURE_SYS",
|
|
source: "WITHINGS",
|
|
deletedAt: null,
|
|
measuredAt: { gte: since },
|
|
},
|
|
orderBy: { measuredAt: "asc" },
|
|
select: { value: true },
|
|
});
|
|
systolic = rows.map((r) => Number(r.value));
|
|
const diaRows = await prisma.measurement.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
type: "BLOOD_PRESSURE_DIA",
|
|
source: "WITHINGS",
|
|
deletedAt: null,
|
|
measuredAt: { gte: since },
|
|
},
|
|
orderBy: { measuredAt: "asc" },
|
|
select: { value: true },
|
|
});
|
|
diastolic = diaRows.map((r) => Number(r.value));
|
|
const ramipril = await prisma.medication.findFirstOrThrow({
|
|
where: { userId: user.id, name: "Ramipril" },
|
|
select: { id: true },
|
|
});
|
|
const changes = await prisma.medicationDoseChange.findMany({
|
|
where: { medicationId: ramipril.id, effectiveFrom: { gte: since } },
|
|
select: { effectiveFrom: true, doseValue: true },
|
|
});
|
|
doseChanges = changes.map((c) => ({
|
|
effectiveFrom: c.effectiveFrom,
|
|
doseValue: Number(c.doseValue),
|
|
}));
|
|
means = [];
|
|
for (let i = 6; i < systolic.length; i++) {
|
|
const week = systolic.slice(i - 6, i + 1);
|
|
means.push(week.reduce((a, b) => a + b, 0) / week.length);
|
|
}
|
|
}, 180_000);
|
|
|
|
describe("demo seed, short-window shape", () => {
|
|
it("gives the 7-day systolic mean over 30 days a rise and fall that reads", () => {
|
|
expect(systolic.length).toBeGreaterThanOrEqual(28);
|
|
const swing = Math.max(...means) - Math.min(...means);
|
|
expect(swing).toBeGreaterThanOrEqual(18);
|
|
expect(Math.max(...systolic)).toBeGreaterThanOrEqual(140);
|
|
// One mmHg past 148 and the padded axis rounds out to 50-200 in steps of
|
|
// 50, which flattens the rise again.
|
|
expect(Math.max(...systolic)).toBeLessThanOrEqual(148);
|
|
expect(Math.max(...diastolic)).toBeGreaterThanOrEqual(88);
|
|
});
|
|
|
|
it("keeps the day-to-day steps small enough for the 7-day mean to draw an arc", () => {
|
|
const steps = systolic.slice(1).map((v, i) => Math.abs(v - systolic[i]));
|
|
const sorted = [...steps].sort((a, b) => a - b);
|
|
expect(sorted[Math.floor(sorted.length / 2)]).toBeLessThanOrEqual(3);
|
|
});
|
|
|
|
it("leaves the median of the window in the settled band", () => {
|
|
// The usual range is the median ± 3 MAD of the last 30 days. A stretch
|
|
// that fills half the window moves that median onto the stretch and
|
|
// widens the band to something like 87-181 mmHg. The seed's own spread
|
|
// reaches about ±22 in one run of 150, so the bound sits at 24, still far
|
|
// below what a stretch that long produces.
|
|
const sorted = [...systolic].sort((a, b) => a - b);
|
|
const median = sorted[Math.floor(sorted.length / 2)];
|
|
expect(median).toBeLessThanOrEqual(124);
|
|
const deviations = systolic
|
|
.map((v) => Math.abs(v - median))
|
|
.sort((a, b) => a - b);
|
|
const mad = deviations[Math.floor(deviations.length / 2)];
|
|
expect(3 * 1.4826 * mad).toBeLessThanOrEqual(24);
|
|
});
|
|
|
|
it("raises ramipril on the day the stretch turns", () => {
|
|
expect(doseChanges).toHaveLength(1);
|
|
expect(doseChanges[0].doseValue).toBe(10);
|
|
const daysBefore =
|
|
(Date.now() - doseChanges[0].effectiveFrom.getTime()) / 86_400_000;
|
|
expect(daysBefore).toBeGreaterThan(8);
|
|
expect(daysBefore).toBeLessThan(14);
|
|
});
|
|
|
|
it("brings today's systolic reading back down from the stretch", () => {
|
|
expect(systolic.at(-1)).toBeLessThanOrEqual(Math.max(...means) - 15);
|
|
expect(systolic.at(-1)).toBeLessThanOrEqual(125);
|
|
});
|
|
});
|