mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Zod 4's .meta() is immutable: it returns a new schema carrying the metadata and registers that one. Four request bodies in the sharing family called it as a statement and dropped the result, so the route table went on referencing the bare schema, the bodies were emitted inline and anonymous, and the object-level descriptions never reached docs/api/openapi.yaml. One of them carries the clause that scopes every rule on the invitation endpoint to an invited grant, which is the sentence that stops a reader carrying the step-up and the refusals across to a guardian grant. Bind each to a const, reference the const, and correct the level boundary in the invitation's description while it is being published: it still carried the blanket denial that stopped being true when the anamnesis surface opened to a manage delegate. The wire format does not move. Four named components appear and the four bodies point at them; the fields, the types and the requiredness are what they were. A new guard asserts every id this route table declares reaches the built document, because the failing form looks exactly like the working one.
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { buildOpenApiDocument } from "@/lib/openapi/registry";
|
|
|
|
/**
|
|
* Every schema id the sharing route table declares reaches the published
|
|
* document.
|
|
*
|
|
* The failure this exists for looks like nothing at all. `.meta()` in Zod 4 is
|
|
* immutable: it returns a NEW schema carrying the metadata and registers that
|
|
* one, so `schema.meta({ id, description })` written as a statement registers
|
|
* an instance no route references. The generator then emits the request body
|
|
* inline and anonymous, the id never becomes a component, and the object-level
|
|
* description never leaves the source file. Nothing throws, nothing drifts,
|
|
* `openapi:check` stays green, and the sentence somebody wrote for the client
|
|
* team is readable only by someone already reading the code.
|
|
*
|
|
* Four of the sharing bodies were written that way, and one of them carried the
|
|
* clause scoping every rule on the invitation endpoint to an INVITED grant —
|
|
* the exact sentence a reader needs in order not to carry the step-up and the
|
|
* refusals across to a Guardian grant. It was found by reading the artifact
|
|
* instead of the source, which is not a repeatable way to find things.
|
|
*
|
|
* ## What is checked
|
|
*
|
|
* The ids are read out of the route table's own text rather than listed here,
|
|
* so a body added tomorrow is covered without anybody remembering this file.
|
|
* The set is asserted non-empty first: a matcher that silently stopped matching
|
|
* would otherwise report a green run over nothing, which is the same class of
|
|
* defect as the one under test.
|
|
*
|
|
* The document is BUILT rather than read off disk. `scripts/check-openapi.ts`
|
|
* already pins `docs/api/openapi.yaml` to exactly this document and CI fails on
|
|
* drift, so building it here checks the same artifact without parsing a
|
|
* megabyte of YAML.
|
|
*
|
|
* ## What it cannot see
|
|
*
|
|
* Only this route module. The idiom is long-standing across the registry and
|
|
* fifty-seven other ids are declared the same way in modules this file does not
|
|
* read; widening the sweep is a separate change with a separate diff, recorded
|
|
* in the post-release inventory.
|
|
*/
|
|
const ROUTE_TABLE = join(
|
|
process.cwd(),
|
|
"src/lib/openapi/routes/account-sharing.ts",
|
|
);
|
|
|
|
/** Every `id: "…"` the route table declares, in source order. */
|
|
function declaredIds(): string[] {
|
|
const source = readFileSync(ROUTE_TABLE, "utf8");
|
|
return [...source.matchAll(/\bid:\s*"([A-Za-z0-9_]+)"/g)].map(
|
|
(match) => match[1]!,
|
|
);
|
|
}
|
|
|
|
describe("the sharing route table's schema ids reach the document", () => {
|
|
const ids = declaredIds();
|
|
const schemas = buildOpenApiDocument().components?.schemas ?? {};
|
|
|
|
it("has ids to check", () => {
|
|
// The non-zero control. Rename the file, change the quoting style, drop the
|
|
// `id` key convention, and this leg fails instead of the file passing
|
|
// vacuously.
|
|
expect(ids.length).toBeGreaterThan(10);
|
|
expect(ids).toContain("AccountGrantInvite");
|
|
expect(ids).toContain("CreateManagedProfileRequest");
|
|
expect(ids).toContain("InviteManagedProfileGuardianRequest");
|
|
expect(ids).toContain("AccountSwitchRequest");
|
|
});
|
|
|
|
it("publishes every declared id as a named component", () => {
|
|
const missing = ids.filter((id) => !(id in schemas));
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it("publishes the invited-grant scoping clause with the request body", () => {
|
|
// The sentence that was lost, checked where a client would read it rather
|
|
// than where it was written.
|
|
const invite = schemas["AccountGrantInvite"] as
|
|
{ description?: string } | undefined;
|
|
expect(invite?.description ?? "").toMatch(
|
|
/Every sentence here describes an INVITED grant on this endpoint/,
|
|
);
|
|
// And the boundary it states is the corrected one. The stale wording denied
|
|
// every settings surface, which stopped being true when the anamnesis
|
|
// surface opened to a manage delegate.
|
|
expect(invite?.description ?? "").not.toMatch(
|
|
/never reaches[^.]*\bsettings\b/i,
|
|
);
|
|
expect(invite?.description ?? "").toMatch(
|
|
/record content versus account configuration/i,
|
|
);
|
|
});
|
|
});
|