Files
HealthLog/src/__tests__/medication-ingredient-backup-completeness.test.ts
T
Marc-André Bombeck 322652a86e refactor(backup): build the export from per-domain slices
buildFullBackupPayload now asks every domain for its slice at once and
lays the slices out in the file's key order (backup-export-sections.ts),
merging the manifest where it first appears. The measurement, medication,
mood, instance-settings and nutrient readers and serialisers move out of
the payload builder into their domain modules; the records section keeps
its composition and manifest, while labs, illness, allergies, family
history, workouts and documents each export from their own module. The
measurement restore half moves to measurements-restore.ts so the export
graph does not pull in the restore's dependencies.

The file, its counts and their order are unchanged; the golden copies
still match. The measurement completeness guard now reads the domain
module and also checks that every carried column is written back.
2026-10-22 07:46:02 +02:00

96 lines
3.7 KiB
TypeScript

/**
* v1.43 — pair guard binding the `MedicationIngredient` model to both ends of
* the backup: the writer's `ingredients: m.ingredients.map(...)` block in
* `medications-backup.ts` and the nested create in `medications-restore.ts`.
*
* The failure it catches is the one the measurement guard beside it was
* written for: a column added to the composition in a later release, the
* writer or the restore left as "the follow-up", the payload schema's
* `.passthrough()` swallowing it, and the column coming back empty on every
* restored row. The column-by-column round trip
* (`tests/integration/backup-round-trip.test.ts`) proves the same against a
* database; this is the fast edge of it that runs with the unit suite.
*
* Mutation check: delete `enteredUnit: i.enteredUnit,` from the writer and the
* first leg names `enteredUnit`; delete `source: i.source,` from the restore
* and the second leg names `source`.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const ROOT = process.cwd();
const read = (...parts: string[]) => readFileSync(join(ROOT, ...parts), "utf8");
/**
* Columns neither end carries, each with its reason.
* - `userId`: re-derived from the restoring account.
* - `medicationId`: the rows are nested in their medication on both ends, so
* the restore binds them to whatever id the medication got.
* - `updatedAt`: `@updatedAt`, stamped by the restore's own write.
*/
const EXCLUDED: Record<string, string> = {
userId: "owner re-derived from the restoring account",
medicationId: "nested in its medication; bound by the nested create",
updatedAt: "@updatedAt; stamped by the restore's write",
};
function modelColumns(): string[] {
const schema = read("prisma", "schema.prisma");
const start = schema.indexOf("model MedicationIngredient {");
expect(start).toBeGreaterThan(-1);
const block = schema.slice(start, schema.indexOf("\n}", start));
const out: string[] = [];
for (const raw of block.split("\n").slice(1)) {
const line = raw.trim();
if (!line || line.startsWith("//") || line.startsWith("@@")) continue;
const m = /^([a-zA-Z_]\w*)\s+([A-Za-z_]\w*)/.exec(line);
if (!m || line.includes("@relation")) continue;
out.push(m[1]);
}
return out;
}
/** The keys one end's ingredient block names, read off `<key>: i.<field>`. */
function blockKeys(source: string, opener: string): Set<string> {
const start = source.indexOf(opener);
expect(start, `${opener} not found`).toBeGreaterThan(-1);
const end = source.indexOf("})),", start);
expect(end).toBeGreaterThan(start);
const body = source.slice(start, end);
return new Set(
[...body.matchAll(/\b([a-zA-Z_]\w*):\s*(?:i\.|new Date\(i\.)/g)].map(
(m) => m[1],
),
);
}
describe("medication ingredient backup completeness", () => {
const columns = modelColumns();
const carried = columns.filter((c) => !(c in EXCLUDED));
it("reads the model's columns", () => {
expect(columns).toEqual(
expect.arrayContaining(["nutrient", "amountPerUnit", "enteredUnit"]),
);
for (const key of Object.keys(EXCLUDED)) expect(columns).toContain(key);
});
it("the writer carries every column", () => {
const keys = blockKeys(
read("src", "lib", "export", "medications-backup.ts"),
"ingredients: m.ingredients.map((i) => ({",
);
expect(carried.filter((c) => !keys.has(c))).toEqual([]);
});
it("the restore writes every column back", () => {
const keys = blockKeys(
read("src", "lib", "export", "medications-restore.ts"),
"create: m.ingredients.map((i) => ({",
);
expect(carried.filter((c) => !keys.has(c))).toEqual([]);
});
});