mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 22:50:51 +00:00
`/` runs its reads server-side and hands them to TanStack through a HydrationBoundary, so the first HTML already carries real tiles. React threw that away on every cold load. The app shell hydrates first and its mount answers `/api/auth/me`; the dashboard sits below `loading.tsx`, hydrates later, and its first client render therefore read an account the server never had. That disagreed with the streamed HTML, React reported #418 and re-rendered the whole page. The prefetch paid for its queries and delivered none of its paint. The account payload is now withheld from the hydration render wherever it is only rendered. `useAccountOnceMounted()` answers null until the component has mounted, which is what the server rendered, and hands the payload over on the next render. The dashboard page, the caregiver card, the getting-started checklist, the achievements tile and the unit labels read it that way. `useAuth` itself still reads the cache, because the document vault redirects off `user.modules` and a first render claiming "no account" bounces it to `/`. Two tiles disagreed without any account involved. A query reports `isLoading` true on the client's first render and false on the server, so both now gate on whether the cell holds an answer. Nothing in CI ran the shipped configuration: the suite's server turns the prefetch off so route mocks keep governing the dashboard. A second server runs it on, and one spec asserts that no hydration error is reported and that the tile node the server streamed is still the node in the DOM once the page settles. Measured over nine cold loads with the CPU throttled 4x: seven bailouts before and none after, median LCP 869 ms before and 145 ms after. FCP and the time to the first tile in the DOM are unchanged, because the streamed paint was never the problem. Keeping it was.
213 lines
8.5 KiB
TypeScript
213 lines
8.5 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. Either reading of
|
|
// it counts: a surface that paints inside a streamed page boundary takes
|
|
// `useUnitDisplayOnceMounted()`, which is the same sugar with the
|
|
// preference withheld from the hydration render.
|
|
const offenders = RAW_SCALE_SURFACES.filter(
|
|
(rel) => !/\buseUnitDisplay(OnceMounted)?\(\)/.test(read(rel)),
|
|
);
|
|
expect(offenders).toEqual([]);
|
|
});
|
|
});
|