Files
HealthLog/src/__tests__/i18n-reference-matcher.test.ts
T
Marc-André Bombeck d8a3c08e12 fix(i18n): stop one key template from whitelisting a whole namespace
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.
2026-07-25 16:23:58 +02:00

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);
});
});
});