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.
210 lines
8.2 KiB
TypeScript
210 lines
8.2 KiB
TypeScript
/**
|
|
* Guard for the converted-value precision class (issue #627, second half).
|
|
*
|
|
* Every entry in the display-transform registry declares the `decimals` its
|
|
* unit is read at. Before v1.32.39 that number was carried all the way to the
|
|
* render layer and read by exactly one screen out of all the ones that show a
|
|
* converted value, so everywhere else the raw product of a kilogram and
|
|
* 2.20462262185 reached the surface unrounded. The fix moved the rounding into
|
|
* the conversion helpers, which means a surface gets a correctly-scaled number
|
|
* whether or not it remembers to ask.
|
|
*
|
|
* This file guards the two ways that could be undone:
|
|
*
|
|
* 1. A PROPERTY over the whole registry — every branch, swept over a spread
|
|
* of raw values, must come out already at its declared grain, and a
|
|
* non-rescaling branch must come out byte-identical. A property test is
|
|
* the honest shape here: the thing being guaranteed is arithmetic, and a
|
|
* structural sweep for "did the surface remember to round" is exactly the
|
|
* per-call-site discipline this change exists to abolish.
|
|
* 2. A STRUCTURAL freeze on the two ways to get an unrounded converted value
|
|
* anyway — importing the documented escape hatch, or hand-rolling
|
|
* `value * transform.factor + transform.offset` at a call site. Both sets
|
|
* are pinned to a named allowlist, so a new converted-value surface
|
|
* cannot quietly join them.
|
|
*/
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { walkSourceFiles } from "./helpers/source-files";
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import {
|
|
applyDisplayTransform,
|
|
applyDisplayTransformDelta,
|
|
getDisplayTransform,
|
|
transformRescales,
|
|
TRANSFORMED_TYPES,
|
|
} from "@/lib/measurements/display-transform";
|
|
|
|
const SRC = join(process.cwd(), "src");
|
|
|
|
/** The registry itself owns the arithmetic; it is not a call site. */
|
|
const REGISTRY = "lib/measurements/display-transform.ts";
|
|
|
|
/**
|
|
* The one module allowed to import the unrounded escape hatch. The target
|
|
* adapter needs the unrounded pound edge of a canonical guardrail so it can
|
|
* round it INWARD — rounding to the nearest tenth first gives 66.1 lb for the
|
|
* 30 kg floor, which inverts to 29.98 kg and the server rejects it.
|
|
*/
|
|
const UNROUNDED_IMPORTERS = ["lib/targets/target-unit-display.ts"];
|
|
|
|
/**
|
|
* The surfaces allowed to read a transform's raw `factor` / `offset`. All four
|
|
* do it for the same reason: the chart takes the multiplier as a prop, folds
|
|
* it into its own series at the single read boundary, and formats the result
|
|
* itself. Everything those files RENDER directly goes through the rounding
|
|
* helpers. Adding a file here means arguing that its number never reaches a
|
|
* screen unrounded.
|
|
*/
|
|
const RAW_SCALE_SURFACES = [
|
|
"app/page-client.tsx",
|
|
"app/insights/weight/page.tsx",
|
|
"components/insights/healthkit-metric-page.tsx",
|
|
"components/insights/device-score-tile.tsx",
|
|
];
|
|
|
|
/** Matches a `factor` / `offset` read off a display transform. */
|
|
const RAW_SCALE_READ =
|
|
/\b\w*[Tt]ransform(?:For\([^)]*\))?\??\.(factor|offset)\b/;
|
|
|
|
function sourceFiles(): string[] {
|
|
return walkSourceFiles(SRC, { floor: 3000 }).filter(
|
|
(rel) =>
|
|
// Never walk the Prisma client, and skip the suites that legitimately
|
|
// exercise the raw arithmetic they are asserting about.
|
|
!rel.startsWith("generated/") && !rel.includes("__tests__/"),
|
|
);
|
|
}
|
|
|
|
function read(rel: string): string {
|
|
return readFileSync(join(SRC, rel), "utf8");
|
|
}
|
|
|
|
describe("display-transform rounding guard — the registry property", () => {
|
|
const RAW_VALUES = [
|
|
0, 1, 1.3, 10, 36.6, 36.66666, 37, 78.45, 83.99981845627636, 84, 95.25, 210,
|
|
1609.344, 8046.72, -0.5, -12.3456789,
|
|
];
|
|
|
|
it("sweeps a non-empty registry", () => {
|
|
// Guards against a set that silently empties out (a green void).
|
|
expect(TRANSFORMED_TYPES.size).toBeGreaterThan(5);
|
|
});
|
|
|
|
it("declares a usable decimal count on every branch", () => {
|
|
for (const type of TRANSFORMED_TYPES) {
|
|
for (const preference of ["metric", "imperial"] as const) {
|
|
const { decimals } = getDisplayTransform(type, preference);
|
|
expect(Number.isInteger(decimals)).toBe(true);
|
|
expect(decimals).toBeGreaterThanOrEqual(0);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("never returns a converted value finer than its declared decimals", () => {
|
|
const offenders: string[] = [];
|
|
for (const type of TRANSFORMED_TYPES) {
|
|
for (const preference of ["metric", "imperial"] as const) {
|
|
const transform = getDisplayTransform(type, preference);
|
|
if (!transformRescales(transform)) continue;
|
|
const scale = 10 ** transform.decimals;
|
|
for (const raw of RAW_VALUES) {
|
|
for (const [label, shown] of [
|
|
["absolute", applyDisplayTransform(raw, transform)],
|
|
["delta", applyDisplayTransformDelta(raw, transform)],
|
|
] as const) {
|
|
// Re-rounding at the declared grain must be a no-op: the value
|
|
// carries no more precision than the transform says it should.
|
|
if (Math.round(shown * scale) / scale !== shown) {
|
|
offenders.push(
|
|
`${type}/${preference} ${label} ${raw} → ${shown}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it("leaves a non-rescaling branch byte-identical", () => {
|
|
// The metric branch only relabels the symbol; the number it shows IS the
|
|
// stored number, and a stored 78.45 kg is not conversion noise.
|
|
const offenders: string[] = [];
|
|
for (const type of TRANSFORMED_TYPES) {
|
|
for (const preference of ["metric", "imperial"] as const) {
|
|
const transform = getDisplayTransform(type, preference);
|
|
if (transformRescales(transform)) continue;
|
|
for (const raw of RAW_VALUES) {
|
|
if (applyDisplayTransform(raw, transform) !== raw) {
|
|
offenders.push(`${type}/${preference} absolute ${raw}`);
|
|
}
|
|
if (applyDisplayTransformDelta(raw, transform) !== raw) {
|
|
offenders.push(`${type}/${preference} delta ${raw}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it("leaves an untransformed type byte-identical", () => {
|
|
for (const type of ["PULSE", "BLOOD_GLUCOSE", "BODY_MASS_INDEX"]) {
|
|
const transform = getDisplayTransform(type, "imperial");
|
|
for (const raw of RAW_VALUES) {
|
|
expect(applyDisplayTransform(raw, transform)).toBe(raw);
|
|
expect(applyDisplayTransformDelta(raw, transform)).toBe(raw);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("display-transform rounding guard — the bypass freeze", () => {
|
|
it("sweeps a non-empty source tree", () => {
|
|
expect(sourceFiles().length).toBeGreaterThan(500);
|
|
});
|
|
|
|
it("confines the unrounded escape hatch to its documented importers", () => {
|
|
const importers = sourceFiles().filter(
|
|
(rel) =>
|
|
rel !== REGISTRY &&
|
|
read(rel).includes("applyDisplayTransformUnrounded"),
|
|
);
|
|
expect(importers.sort()).toEqual([...UNROUNDED_IMPORTERS].sort());
|
|
});
|
|
|
|
it("confines hand-rolled transform scaling to the chart-feeding surfaces", () => {
|
|
const offenders = sourceFiles().filter(
|
|
(rel) =>
|
|
rel !== REGISTRY &&
|
|
!UNROUNDED_IMPORTERS.includes(rel) &&
|
|
RAW_SCALE_READ.test(read(rel)),
|
|
);
|
|
expect(offenders.sort()).toEqual([...RAW_SCALE_SURFACES].sort());
|
|
});
|
|
|
|
it("every allowlisted surface reads the raw scale to feed a chart", () => {
|
|
// The justification for holding the raw multiplier at all: the chart
|
|
// takes it as a prop. A file that stops passing it has no reason to be
|
|
// reading `.factor` any more and belongs back behind the helpers.
|
|
// The prop as it is actually passed — a bare substring would still
|
|
// match a renamed identifier that no longer reaches any chart.
|
|
const offenders = RAW_SCALE_SURFACES.filter(
|
|
(rel) => !/\bvalueScale=/.test(read(rel)),
|
|
);
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
|
|
it("every allowlisted surface still converts its rendered values", () => {
|
|
// The chart formats its own series; anything these files render outside
|
|
// the chart must come back through the preference hook.
|
|
const offenders = RAW_SCALE_SURFACES.filter(
|
|
(rel) => !read(rel).includes("useUnitDisplay()"),
|
|
);
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
});
|