Files
HealthLog/src/__tests__/unit-preference-display-guard.test.ts
T
Marc-André Bombeck 1ae137eb84 test(guards): sweep the directories the structural guards were skipping
`fs.globSync` never matches a leading dot, so every guard built on
`**/*.{ts,tsx}` walked past `src/app/.well-known` — the two OAuth
discovery documents and the apple-app-site-association handler sat
outside fourteen frozen-set sweeps. An unlisted `getSession()` planted
in the authorization-server route left the session-surface guard at nine
passed; it fails now.

Move those fourteen onto one shared walker built on `readdirSync` with
`recursive`, which has no dot rule, and give it a floor every caller has
to state, so a walk that comes back too small throws instead of
reporting a clean tree.

Give the same floor to the guards that compared a discovered set against
a frozen list with nothing asserting the set had been read: gutting the
walker in the inventory-write guard left its invariant test green,
because zero offenders reads exactly like no offenders. Raise the
migration-count floor off one, and assert the locale bundles are found
before the per-locale cases are generated from them.

Document, at each walker that prunes on purpose, what is pruned and that
dot-prefixed directories are not.
2026-08-02 16:21:19 +02:00

196 lines
7.6 KiB
TypeScript

/**
* Structural guard (a tripwire, not a proof) for the metric/imperial unit
* fix (issue #627). The dangerous failure mode is a PARTIAL fix: entry converts
* but a display surface still hardcodes the canonical unit, so an imperial user
* sees converted-then-mislabelled soup. This walks the dashboard + insight page
* sources and asserts none of them passes a transformed type's DISPLAY UNIT
* (kg / lb / cm / in / °C / °F / km/h / mph / km / mi) or a hardcoded value
* scale as a literal prop — those must resolve from the user's preference
* through `useUnitDisplay()` instead.
*
* Mutation check: add `unit="kg"` back to any swept page and this goes red.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { walkSourceFiles } from "./helpers/source-files";
import { describe, it, expect } from "vitest";
import {
getDisplayTransform,
TRANSFORMED_TYPES,
} from "@/lib/measurements/display-transform";
const SRC = join(process.cwd(), "src");
/** The set of display-unit strings a transformed type can render. */
const TRANSFORMED_UNITS: ReadonlySet<string> = new Set(
[...TRANSFORMED_TYPES].flatMap((type) => [
getDisplayTransform(type, "metric").displayUnit,
getDisplayTransform(type, "imperial").displayUnit,
]),
);
/**
* v1.32.27 — the target/threshold subsystem. These three files are the
* coupled display+edit unit: the reference panel that RENDERS a target
* band, the sheet that EDITS it, and the settings editor that edits the
* same thresholds from the other end. A partial wire here is worse than
* none — a converted display over a canonical save silently rewrites
* the user's stored target — so they are swept for hardcoded units AND
* asserted to route through the preference hook.
*/
const TARGET_SURFACES = [
join(SRC, "components", "insights", "metric-target-summary.tsx"),
join(SRC, "components", "targets", "target-edit-sheet.tsx"),
join(SRC, "components", "settings", "thresholds-editor-section.tsx"),
];
/**
* v1.32.30 — the profile height surfaces. Height is not a
* `MeasurementType`, so `display-transform.ts` never reached it and it
* stayed in centimetres for two releases after everything else moved.
* These three are the coupled entry unit: the two forms that write
* `User.heightCm` and the control they share. A partial wire here is
* the same failure the target block guards against — a converted field
* over a canonical save silently rewrites the stored height.
*/
const HEIGHT_SURFACES = [
join(SRC, "components", "onboarding", "baseline-form.tsx"),
join(SRC, "components", "settings", "account-section", "index.tsx"),
];
const HEIGHT_CONTROL = join(
SRC,
"components",
"profile",
"height-field-control.tsx",
);
/**
* Swept surfaces: the dashboard client, every insights sub-page, and the
* target/threshold surfaces.
*/
function sweptFiles(): string[] {
return [
join(SRC, "app", "page-client.tsx"),
...walkSourceFiles(SRC, { floor: 700, extensions: [".tsx"] })
.filter(
(rel) => rel.startsWith("app/insights/") && rel.endsWith("/page.tsx"),
)
.map((rel) => join(SRC, rel)),
...TARGET_SURFACES,
];
}
describe("unit-preference display guard", () => {
it("sweeps a non-empty set of surfaces", () => {
// Guards against a glob that silently matches nothing (a green void).
expect(sweptFiles().length).toBeGreaterThan(10);
});
it("no swept surface hardcodes a transformed type's display unit", () => {
const offenders: string[] = [];
for (const file of sweptFiles()) {
const src = readFileSync(file, "utf8");
for (const unit of TRANSFORMED_UNITS) {
// Match `unit="kg"` / `yAxisUnit="°C"` exactly (closing quote pinned so
// `unit="kg/m²"` — the excluded BMI unit — never trips it).
for (const prop of ["unit", "yAxisUnit"]) {
if (src.includes(`${prop}="${unit}"`)) {
offenders.push(`${file}: ${prop}="${unit}"`);
}
}
}
}
expect(offenders).toEqual([]);
});
it("no insights page hardcodes a numeric valueScale literal", () => {
// Transformed metrics resolve their scale from the transform; a literal
// `valueScale={3.6}` is the pre-fix hand-rolled scaling that double-scales.
const literal = /valueScale=\{\s*-?\d/;
const offenders = sweptFiles().filter((file) =>
literal.test(readFileSync(file, "utf8")),
);
expect(offenders).toEqual([]);
});
it("every target/threshold surface resolves its unit from the preference", () => {
// The tripwire for a silent un-wire: delete the hook import from any
// of the three and the surface falls back to canonical kilograms
// while its sibling still shows pounds.
// The call site, not the import — swapping the hook out for a
// hardcoded "metric" literal while leaving the import behind is
// exactly the un-wire this has to catch.
const offenders = TARGET_SURFACES.filter(
(file) => !readFileSync(file, "utf8").includes("useUnitDisplay()"),
);
expect(offenders).toEqual([]);
});
it("every target/threshold surface converts through the shared adapter", () => {
// One adapter owns seed, guardrail, save, and unit label for all
// three surfaces. A surface that hand-rolls its own conversion (or
// announces `METRIC_BOUNDS[metric].unit`, the canonical symbol)
// is how the display and the edit halves drift apart.
const offenders: string[] = [];
for (const file of TARGET_SURFACES) {
const src = readFileSync(file, "utf8");
if (!src.includes("resolveTargetUnitAdapter")) {
offenders.push(`${file}: no adapter`);
}
if (/METRIC_BOUNDS\[[^\]]+\]\.unit/.test(src)) {
offenders.push(`${file}: canonical bound unit rendered`);
}
}
expect(offenders).toEqual([]);
});
it("every height entry surface resolves through the profile adapter", () => {
// Same tripwire as the target block: drop the hook or the adapter
// from one form and it falls back to centimetres while its sibling
// shows feet and inches.
const offenders: string[] = [];
for (const file of HEIGHT_SURFACES) {
const src = readFileSync(file, "utf8");
if (!src.includes("useUnitDisplay()")) {
offenders.push(`${file}: no preference hook`);
}
if (!src.includes("resolveHeightUnitAdapter")) {
offenders.push(`${file}: no height adapter`);
}
if (!src.includes("HeightFieldControl")) {
offenders.push(`${file}: hand-rolled height input`);
}
// The canonical centimetre guardrails must come from the
// adapter's inward-rounded bounds, never from a literal.
if (/min=\{50\}|max=\{300\}/.test(src)) {
offenders.push(`${file}: hardcoded centimetre guardrail`);
}
}
expect(offenders).toEqual([]);
});
it("the height control announces its units from the bundle", () => {
const src = readFileSync(HEIGHT_CONTROL, "utf8");
// A quoted "cm" / "ft" / "in" here is a hardcoded unit label; every
// unit string on this control resolves through `t(...)`.
expect(src).not.toMatch(/["'](cm|ft|in)["']/);
expect(src).toContain('t("common.feet")');
expect(src).toContain('t("common.inches")');
});
it("every transformed type carries both a metric and an imperial branch", () => {
for (const type of TRANSFORMED_TYPES) {
const metric = getDisplayTransform(type, "metric");
const imperial = getDisplayTransform(type, "imperial");
expect(metric.displayUnit).toBeTruthy();
expect(imperial.displayUnit).toBeTruthy();
expect(Number.isFinite(metric.factor)).toBe(true);
expect(Number.isFinite(imperial.factor)).toBe(true);
}
});
});