Files
HealthLog/src/__tests__/list-cap-reader-guard.test.ts
T
Marc-André Bombeck 2be262d574 refactor(api): drop apiPostList, which no capped list reads
Every capped route is read with GET, so the POST reader had no caller
and failed the dead-code gate. The list-cap guard now binds a capped
route to apiGetList alone, which is the reader that exists.
2026-10-20 10:58:03 +02:00

118 lines
4.0 KiB
TypeScript

/**
* Structural guard: every list the server caps has a client that reads the
* cap.
*
* A capped list says so on the envelope (`apiListSuccess`, `meta.truncated`
* / `meta.limit`). The web client read only `data` (`apiGet`), so the signal
* reached nobody: twelve routes sent it and no surface showed it. A capped
* route is now bound to a reader, a production file outside the API tree
* that fetches the route's path through `apiGetList`, the one client reader
* of the cap, which a surface pairs with `ListCapNotice`.
*
* The routes are derived from the tree (every route file that answers with
* `apiListSuccess`), never listed by hand, so a newly capped route fails here
* until something reads its cap. Comments are stripped first; the planted
* cases prove the matchers fire.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { walkSourceFiles } from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
/** Capped routes with no list on screen, and why. */
const NO_LIST_ON_SCREEN: Record<string, string> = {
"/api/personal-records":
"The web reads it for one decorative badge (a record set in the last 30 days), not as a list; the native client owns the list.",
};
function stripComments(text: string): string {
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
}
/** `app/api/a/[id]/b/route.ts` → `/api/a/[id]/b`. */
function routePathOf(file: string): string {
return `/${file.replace(/^app\//, "").replace(/\/route\.tsx?$/, "")}`;
}
/** A matcher for the route's path as a client spells it. */
function clientPathPattern(route: string): RegExp {
const body = route
.split("/")
.map((seg) =>
/^\[.+\]$/.test(seg)
? "\\$\\{[^}]+\\}"
: seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
)
.join("/");
return new RegExp(
`\\bapiGetList\\s*<[\\s\\S]{0,200}?>\\s*\\(\\s*["\`]${body}(?:[?"\`])|\\bapiGetList\\s*\\(\\s*["\`]${body}(?:[?"\`])`,
);
}
describe("capped lists have a reader", () => {
const files = walkSourceFiles(SRC, { floor: 3000 }).filter(
(file) =>
!file.startsWith("generated/") &&
!file.includes("__tests__/") &&
!/\.test\.tsx?$/.test(file),
);
const read = (file: string) =>
stripComments(readFileSync(join(SRC, file), "utf8"));
const cappedRoutes = files
.filter((f) => /^app\/api\/.+\/route\.tsx?$/.test(f))
.filter((f) => /\bapiListSuccess\s*\(/.test(read(f)))
.map(routePathOf)
.sort();
const clients = files
.filter((f) => !f.startsWith("app/api/"))
.map((f) => read(f));
it("the matcher binds a client spelling to its route", () => {
expect(
clientPathPattern("/api/medications/[id]/side-effects").test(
"apiGetList<{ items: Row[] }>(`/api/medications/${medicationId}/side-effects?from=1`)",
),
).toBe(true);
expect(
clientPathPattern("/api/allergies").test(
'apiGetList<AllergyDTO[]>("/api/allergies")',
),
).toBe(true);
// A plain `apiGet` reads `data` only and does not count.
expect(
clientPathPattern("/api/allergies").test(
'apiGet<AllergyDTO[]>("/api/allergies")',
),
).toBe(false);
// A longer path that shares the prefix does not count either.
expect(
clientPathPattern("/api/allergies").test(
'apiGetList<AllergyDTO>("/api/allergies/x")',
),
).toBe(false);
});
it("finds the capped routes", () => {
expect(cappedRoutes.length).toBeGreaterThanOrEqual(12);
});
it("binds every capped route to a client that reads its cap", () => {
const unread = cappedRoutes.filter(
(route) =>
!(route in NO_LIST_ON_SCREEN) &&
!clients.some((code) => clientPathPattern(route).test(code)),
);
expect(unread).toEqual([]);
});
it("keeps the exemptions to routes that are still capped", () => {
for (const route of Object.keys(NO_LIST_ON_SCREEN)) {
expect(cappedRoutes).toContain(route);
}
});
});