mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
feat(medications): let a delegate manage a composition, bypass the proxy for label uploads, and keep the guards current
This commit is contained in:
1 parent
eb3cc4237f
commit
9d32a8acb0
19 files changed
+293
-30
No files matched your search
@@ -139,3 +139,77 @@ describe("insights unit-literal guard", () => {
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.43 — no nutrient unit spelled outside the nutrient catalogue.
|
||||
*
|
||||
* A supplement's composition converts an entered amount to the catalogue's
|
||||
* canonical unit, and the nutrient reads judge a day against an upper limit
|
||||
* in that unit. A µg/mg slip is a 1000× error, and the way it gets in is a
|
||||
* second place that spells a unit or a factor: a converter that knows "IU"
|
||||
* itself, a read that compares against "mg". So the rule is that the code
|
||||
* under `src/lib/nutrients/` and `src/lib/medications/ingredients/` names no
|
||||
* unit as a string literal; units and factors come from
|
||||
* `src/lib/nutrients/catalog.ts` (`NUTRIENT_UNIT`, `inputUnits`,
|
||||
* `toCanonicalNutrientAmount`, `parseNutrientInputUnit`), the one file allowed
|
||||
* to spell them.
|
||||
*/
|
||||
const NUTRIENT_UNIT_DIRS = [
|
||||
join(process.cwd(), "src/lib/nutrients"),
|
||||
join(process.cwd(), "src/lib/medications/ingredients"),
|
||||
];
|
||||
const NUTRIENT_CATALOG_FILE = join(
|
||||
process.cwd(),
|
||||
"src/lib/nutrients/catalog.ts",
|
||||
);
|
||||
|
||||
/** A string literal that is nothing but a nutrient unit. */
|
||||
const UNIT_LITERAL =
|
||||
/(["'`])(mg|ug|µg|μg|mcg|g|kg|ml|mL|l|L|IU|IE|UI|iu|ie|ui)\1/g;
|
||||
|
||||
function sourceFilesUnder(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry);
|
||||
if (statSync(full).isDirectory()) {
|
||||
if (entry === "__tests__") continue;
|
||||
out.push(...sourceFilesUnder(full));
|
||||
} else if (/\.tsx?$/.test(entry) && !/\.test\.tsx?$/.test(entry)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe("nutrient unit-literal guard", () => {
|
||||
const files = NUTRIENT_UNIT_DIRS.flatMap(sourceFilesUnder);
|
||||
|
||||
it("scans the nutrient and composition code", () => {
|
||||
expect(files.length).toBeGreaterThanOrEqual(6);
|
||||
expect(files).toContain(NUTRIENT_CATALOG_FILE);
|
||||
});
|
||||
|
||||
it("the matcher sees the catalogue's own spellings", () => {
|
||||
// The catalogue is where units live, so a matcher that finds nothing
|
||||
// there would make the next assertion vacuously true.
|
||||
const catalog = readFileSync(NUTRIENT_CATALOG_FILE, "utf8");
|
||||
const seen = new Set([...catalog.matchAll(UNIT_LITERAL)].map((m) => m[2]));
|
||||
for (const unit of ["mg", "ug", "IU", "ml"]) {
|
||||
expect(seen.has(unit), `expected to see "${unit}" in the catalogue`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("names no unit outside the catalogue", () => {
|
||||
const offenders: string[] = [];
|
||||
for (const file of files) {
|
||||
if (file === NUTRIENT_CATALOG_FILE) continue;
|
||||
const source = readFileSync(file, "utf8");
|
||||
for (const match of source.matchAll(UNIT_LITERAL)) {
|
||||
offenders.push(`${file.slice(file.indexOf("src/"))}: ${match[0]}`);
|
||||
}
|
||||
}
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -83,6 +83,8 @@ const INTAKE_MAY_SEE_EVERY_ROW: Record<string, string> = {
|
||||
"folds duplicate rows of doses already recorded",
|
||||
"lib/doctor-report/collect.ts":
|
||||
"administration ledger lists recorded doses; rates gate via expectsDoses",
|
||||
"lib/medications/ingredients/recompute.ts":
|
||||
"sums the nutrients of doses that were taken; a recorded dose counts whatever the switch says",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,6 +96,10 @@ const MAY_READ_EVERY_ROW: Record<string, string> = {
|
||||
// as stored, schedule and switch included.
|
||||
"lib/export/full-backup-payload.ts": "backup carries the stored rows",
|
||||
"lib/export/restore-backup.ts": "restore writes the stored rows back",
|
||||
// v1.43 — the units a taken dose consumed, resolved per slot exactly as
|
||||
// the inventory does. It reads only doses already recorded as taken.
|
||||
"lib/medications/ingredients/recompute.ts":
|
||||
"units per slot of doses already taken",
|
||||
"app/api/export/route.ts": "data export of the stored record",
|
||||
"app/api/export/medications/route.ts": "medication CSV export of the record",
|
||||
// Intake WRITE helpers: they run only when a dose is being recorded, bind
|
||||
|
||||
@@ -74,7 +74,9 @@ describe("proxy matcher", () => {
|
||||
const routes = apiRoutePaths();
|
||||
expect(routes.length).toBeGreaterThan(300);
|
||||
|
||||
for (const path of PROXY_BYPASS_ROUTES) {
|
||||
for (const route of PROXY_BYPASS_ROUTES) {
|
||||
// A dynamic segment is filled the way `apiRoutePaths` fills it.
|
||||
const path = route.replace(/\[[^\]]+\]/g, "x");
|
||||
expect(routes, `${path} is not a route`).toContain(path);
|
||||
expect(re.test(path), `${path} still passes the proxy`).toBe(false);
|
||||
expect(re.test(`${path}/`), `${path}/ still passes the proxy`).toBe(
|
||||
@@ -98,6 +100,8 @@ describe("proxy matcher", () => {
|
||||
"/api/user/avatar/abc",
|
||||
"/api/documents/inbound/abc/extract",
|
||||
"/api/import/csv/preview",
|
||||
// The composition PUT beside the label upload stays behind the proxy.
|
||||
"/api/medications/abc/ingredients",
|
||||
]) {
|
||||
expect(re.test(path), path).toBe(true);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user