mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
`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.
196 lines
7.6 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|