mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
The reverse-coverage guard asserts that every key in the bundle is reachable from a call site. It had stopped doing that for most of the bundle. A template whose only static anchor was a namespace root registered that root as a subtree base, so a single per-provider OAuth key marked every key under `settings.*` reachable, and the same shape did it for `measurementReminders.*`. A cache-key string that happens to start with a namespace word had quietly done it for `insights.*` too. The guard stayed green throughout, which is the part worth noticing: a pair guard degrades into a one-end guard without any signal. Extract the matcher into a helper with its own unit tests, and change what it accepts. A template carrying both a literal prefix and a literal suffix now records the pair and requires both ends to match, so the OAuth template covers the provider keys and nothing else. A template with no suffix whose anchor is a bare namespace no longer whitelists anything — it fails the guard by name, with the two honest remedies in the message. The same door is used by the template, assignment and concatenation arms, so none of them can reopen it. Template bodies are read by scanning forward from a known opening backtick rather than by pairing backticks across the tree, which was drifting and dropping whole files out of the scan. Three call sites build their keys whole instead of interpolating onto a namespace root: the two measurement-reminder due helpers, the mental-health option labels, and the OAuth error copy, which now names its per-provider base. Two audit-log action names that merely look like keys carry a written exemption; the API error catalog is a runtime lookup and is allowlisted with its reason. With the matcher honest, 149 keys had no call site left. They belong to an explainer popover, a hypnogram, a passkey table and an insights hero that were all rebuilt or removed, and they are deleted from all six locales. The explainer guard stops pinning its title leaves, which no surface has rendered since the popover went.
148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
/**
|
|
* v1.32.36 — unit tests for the reverse-i18n reachability matcher.
|
|
*
|
|
* The reverse-coverage guard is a pair guard, and a pair guard is worthless the
|
|
* moment its matcher over-accepts: it stays green while proving nothing. That
|
|
* is what happened here — one template anchored at a bare `settings.` marked
|
|
* several thousand keys reachable and nobody could tell from the green run.
|
|
*
|
|
* So the matcher gets its own tests over synthetic sources, where the expected
|
|
* answer is known exactly. The first test below is the regression: it plants
|
|
* the old hole and asserts the collector now refuses it out loud.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import { collectReferences, isReferenced } from "./helpers/i18n-reference";
|
|
|
|
const NAMESPACES = new Set([
|
|
"settings",
|
|
"cycle",
|
|
"measurementReminders",
|
|
"notifications",
|
|
"insights",
|
|
]);
|
|
|
|
function run(source: string, nodes: string[] = []) {
|
|
return collectReferences(source, new Set(nodes), NAMESPACES);
|
|
}
|
|
|
|
describe("i18n reference matcher", () => {
|
|
describe("bare top-level namespaces", () => {
|
|
it("refuses a suffix-less template anchored at a bare namespace", () => {
|
|
const { violations } = run("t(`measurementReminders.${due.key}`)");
|
|
|
|
expect(violations).toHaveLength(1);
|
|
expect(violations[0].literal).toContain("measurementReminders.");
|
|
});
|
|
|
|
it("does not silently whitelist the namespace it refused", () => {
|
|
const { ref } = run("t(`measurementReminders.${due.key}`)");
|
|
|
|
expect(isReferenced("measurementReminders.somethingDead", ref)).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("refuses the same shape arriving through the concat arm", () => {
|
|
const { violations, ref } = run('const k = "settings." + provider;');
|
|
|
|
expect(violations).toHaveLength(1);
|
|
expect(violations[0].arm).toBe("concat");
|
|
expect(isReferenced("settings.anythingAtAll", ref)).toBe(false);
|
|
});
|
|
|
|
it("refuses the same shape arriving outside a t() call", () => {
|
|
const { violations } = run("const key = `cycle.${phase}`;");
|
|
|
|
expect(violations[0].arm).toBe("assignment");
|
|
});
|
|
|
|
it("accepts a deep prefix, which confines itself", () => {
|
|
const { violations, ref } = run("t(`cycle.phase.${phase}`)");
|
|
|
|
expect(violations).toHaveLength(0);
|
|
expect(isReferenced("cycle.phase.follicular", ref)).toBe(true);
|
|
expect(isReferenced("cycle.somethingElse", ref)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("pair templates", () => {
|
|
it("covers only leaves matching both ends", () => {
|
|
const { violations, ref } = run(
|
|
"t(`settings.${provider}OauthConnected`)",
|
|
);
|
|
|
|
expect(violations).toHaveLength(0);
|
|
expect(isReferenced("settings.polarOauthConnected", ref)).toBe(true);
|
|
expect(isReferenced("settings.withingsOauthConnected", ref)).toBe(true);
|
|
expect(isReferenced("settings.anythingElse", ref)).toBe(false);
|
|
expect(isReferenced("settings.polarOauthFailed", ref)).toBe(false);
|
|
});
|
|
|
|
it("honours a dot-rooted suffix as a path boundary", () => {
|
|
const { ref } = run("t(`insights.section.${slug}.title`)");
|
|
|
|
expect(isReferenced("insights.section.sleep.title", ref)).toBe(true);
|
|
// `subtitle` ends in "title" but not in ".title".
|
|
expect(isReferenced("insights.section.sleep.subtitle", ref)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("suffix templates", () => {
|
|
it("means nothing without a base another arm established", () => {
|
|
const { ref } = run("t(`${i18nPrefix}SyncResult`)");
|
|
|
|
expect(isReferenced("settings.withingsSyncResult", ref)).toBe(false);
|
|
});
|
|
|
|
it("attaches to a declared i18nPrefix base", () => {
|
|
const { ref } = run(
|
|
'i18nPrefix="settings.withings"\nt(`${i18nPrefix}SyncResult`)',
|
|
);
|
|
|
|
expect(isReferenced("settings.withingsSyncResult", ref)).toBe(true);
|
|
expect(isReferenced("settings.withings", ref)).toBe(true);
|
|
// The declaration is a base, not a subtree licence.
|
|
expect(isReferenced("settings.withingsSomethingDead", ref)).toBe(false);
|
|
expect(isReferenced("settings.polarSyncResult", ref)).toBe(false);
|
|
});
|
|
|
|
it("attaches to a base established by a literal node path", () => {
|
|
const { ref } = run(
|
|
'const p = "insights.walkingSpeed";\nt(`${p}.title`)',
|
|
["insights.walkingSpeed"],
|
|
);
|
|
|
|
expect(isReferenced("insights.walkingSpeed.title", ref)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("literal keys", () => {
|
|
it("covers the key and its subtree", () => {
|
|
const { ref } = run('t("settings.export.title")\nt("cycle.phase")');
|
|
|
|
expect(isReferenced("settings.export.title", ref)).toBe(true);
|
|
expect(isReferenced("cycle.phase.luteal", ref)).toBe(true);
|
|
expect(isReferenced("settings.export.subtitle", ref)).toBe(false);
|
|
});
|
|
|
|
it("covers exactly the three plural tiers of a counted key", () => {
|
|
const { ref } = run('tCount("insights.staleWeeks", n)');
|
|
|
|
expect(isReferenced("insights.staleWeeksOne", ref)).toBe(true);
|
|
expect(isReferenced("insights.staleWeeksFew", ref)).toBe(true);
|
|
expect(isReferenced("insights.staleWeeksOther", ref)).toBe(true);
|
|
expect(isReferenced("insights.staleWeeksMany", ref)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("scope", () => {
|
|
it("ignores an interpolating template that is not namespace-rooted", () => {
|
|
const { violations, ref } = run("const cacheKey = `coach.${userId}`;");
|
|
|
|
expect(violations).toHaveLength(0);
|
|
expect(isReferenced("coach.anything", ref)).toBe(false);
|
|
});
|
|
});
|
|
});
|