Files
HealthLog/tests/integration/medication-administrations.test.ts
Marc-André Bombeck f42934487d test(export): prove the scope holds at the artefact, not just at the switch
Five layers, because the defect had five places to hide.

The catalogue guard checks what the compiler cannot state: every leaf resolves
a label a human can read, every group resolves one, the fenced tier stays last,
and the shipped template intersects the fence at nothing.

The selection guard pins the three minting functions against each other. A
stored blob this build cannot read resolves to the empty selection, never to a
default set — that is what makes the revoked legacy links revocable rather than
silently re-scoped. A request naming an id the server does not know is refused
by name rather than dropped, because dropping it would narrow a scope the
caller did express.

The egress guard freezes the set of aggregator callers with the source of each
one's selection written down. The required brand-typed parameter already stops
a new path compiling without an answer; this stops one being added without a
human writing the answer down, which is the moment a reviewer asks the
question.

The behavioural sweep aggregates every leaf in and out against a fixture that
carries a row for each, and asserts the payload differs. It is parametrised
over the catalogue, so a leaf added later is covered the day it is added. Its
positive control asserts the all-on payload is genuinely populated first, so a
"no difference" result means the gate is inert and not that the account is
empty. Beside it: an unmapped measurement type is excluded rather than passed
through, no table behind an unchosen leaf is queried at all, and an empty
selection produces an empty record.

The artefact layer runs the real route and reads what comes out — the FHIR
bundle, the PDF text and the zip's contents — with a positive control beside
every exclusion.

Mutation-checked: making the gate admit everything reddens the sweep for every
leaf; passing an unmapped type through reddens the fail-closed case; dropping
the mood query's condition reddens the zero-read case; returning the BMI
unconditionally reddens the sweep naming BODY_MASS_INDEX; adding a fenced leaf
to the template reddens the purity guard; removing the panel's seeding block
reddens the first-run and repeat-run cases; dropping the insurance refusal
reddens the share-link case; adding a caller outside the frozen list reddens
the egress guard naming the file.

The panel suite grew past the practice name it was written for and is renamed
for what it now covers: the three states, the fenced tier having no group
control, and the scope line naming its sensitive inclusions.

Integration coverage moves to the leaf model, including the case this release
exists for: an account with data in every domain and a caller that named
nothing gets an empty record.

The OpenAPI contract catches up with the routes — the required selection, the
refused keys, the share summary's new fields, the moved `Patient/$everything`,
the Coverage search and the five typed reads.
2026-07-26 00:45:08 +02:00

144 lines
4.7 KiB
TypeScript

/**
* v1.9.0 — `medicationAdministrations` aggregation + drug-code carry.
*
* Asserts the source rows the FHIR `MedicationAdministration` builder
* consumes are assembled correctly from `MedicationIntakeEvent`:
* - a taken row → `completed` with `effectiveAt = takenAt`,
* - an explicit skip → `not-done` with `effectiveAt = scheduledFor`,
* - a pending / missed slot (no `takenAt`, not skipped) → omitted,
* - a soft-deleted tombstone → omitted,
* - the structured dose-in-effect resolved from `MedicationDoseChange`,
* - the medication's `atcCode` / `rxNormCode` carried onto both the
* `medications[]` concept and each administration row.
*/
import { beforeEach, describe, expect, it } from "vitest";
process.env.ENCRYPTION_KEY ??=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
import { collectDoctorReportData } from "@/lib/doctor-report-data";
import { selectionFromLeaves } from "@/lib/report-selection/selection";
import { getPrismaClient, truncateAllTables } from "./setup";
const RANGE = {
start: new Date("2026-01-01T00:00:00.000Z"),
end: new Date("2026-06-01T00:00:00.000Z"),
days: 152,
};
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
describe("medicationAdministrations aggregation (v1.9.0)", () => {
it("maps acted intakes, omits missed + tombstoned, resolves dose-in-effect", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: { username: "admin-agg", email: "admin-agg@example.test" },
});
const med = await prisma.medication.create({
data: {
userId: user.id,
name: "Mounjaro",
dose: "10mg",
active: true,
treatmentClass: "GLP1",
deliveryForm: "INJECTION",
atcCode: "A10BX10",
rxNormCode: "2601723",
},
});
// Dose-change history: 5mg from Feb, stepped to 10mg from Apr.
await prisma.medicationDoseChange.createMany({
data: [
{
medicationId: med.id,
effectiveFrom: new Date("2026-02-01T00:00:00.000Z"),
doseValue: 5,
doseUnit: "mg",
},
{
medicationId: med.id,
effectiveFrom: new Date("2026-04-01T00:00:00.000Z"),
doseValue: 10,
doseUnit: "mg",
},
],
});
// A taken dose in March (dose-in-effect = 5mg) with a site.
await prisma.medicationIntakeEvent.create({
data: {
userId: user.id,
medicationId: med.id,
scheduledFor: new Date("2026-03-15T08:00:00.000Z"),
takenAt: new Date("2026-03-15T08:12:00.000Z"),
injectionSite: "ABDOMEN_LEFT",
},
});
// A skipped dose in April.
await prisma.medicationIntakeEvent.create({
data: {
userId: user.id,
medicationId: med.id,
scheduledFor: new Date("2026-04-15T08:00:00.000Z"),
skipped: true,
},
});
// A missed slot (no takenAt, not skipped) — must be omitted.
await prisma.medicationIntakeEvent.create({
data: {
userId: user.id,
medicationId: med.id,
scheduledFor: new Date("2026-05-01T08:00:00.000Z"),
},
});
// A soft-deleted (tombstoned) taken dose — must be omitted.
await prisma.medicationIntakeEvent.create({
data: {
userId: user.id,
medicationId: med.id,
scheduledFor: new Date("2026-05-10T08:00:00.000Z"),
takenAt: new Date("2026-05-10T08:05:00.000Z"),
deletedAt: new Date("2026-05-11T00:00:00.000Z"),
},
});
const data = await collectDoctorReportData(
user.id,
RANGE,
selectionFromLeaves([
"MEDICATION_LIST",
"MEDICATION_ADMINISTRATIONS",
"MEDICATION_COMPLIANCE",
]),
);
// Drug codes carried onto the medication concept source.
expect(data.medications[0].atcCode).toBe("A10BX10");
expect(data.medications[0].rxNormCode).toBe("2601723");
const admins = data.medicationAdministrations ?? [];
// Only the taken + skipped rows survive.
expect(admins).toHaveLength(2);
const taken = admins.find((a) => a.status === "completed");
expect(taken).toBeDefined();
expect(taken?.effectiveAt).toBe("2026-03-15T08:12:00.000Z");
expect(taken?.injectionSite).toBe("ABDOMEN_LEFT");
expect(taken?.deliveryForm).toBe("INJECTION");
expect(taken?.atcCode).toBe("A10BX10");
// Dose-in-effect at 2026-03-15 is the 5mg step (not yet 10mg).
expect(taken?.dose).toEqual({ value: 5, unit: "mg" });
const skipped = admins.find((a) => a.status === "not-done");
expect(skipped).toBeDefined();
// A skip uses scheduledFor and records no consumed dose.
expect(skipped?.effectiveAt).toBe("2026-04-15T08:00:00.000Z");
expect(skipped?.dose).toBeNull();
});
});