Files
HealthLog/src/__tests__/sharing-boundary-copy.test.ts
T
aucun6352 f7cc47850a fix(i18n): make the Korean locale actually work end to end
Registering `ko` in the `Locale` union left seven locale-keyed tables
without an entry, so the build could not type-check and the safety
screens, the citation-coverage grader and the reference-range parser
read nothing at all for a Korean reader.

Three failures were not missing keys but wrong assumptions:

- The shared fold decomposes with NFD and strips U+0300-U+036F. Latin
  accents become plain letters, but Hangul decomposes into jamo, which
  are not in that range and survived — so folded Korean text was a jamo
  sequence no precomposed table entry could ever match. It recomposes to
  NFC now; every Latin, Polish and German fold is byte-identical.
- `normaliseLabKey` kept `[a-z0-9]`, which reduced every Korean analyte
  name to the empty string, so the derived index held none of them and a
  Korean cholesterol result reached a FHIR export uncoded.
- The reference-range parser had the hyphen and the dashes but not the
  tilde a Korean lab actually prints (`3.5~5.0`).

The Korean safety banks carry no `\b` next to a Hangul token — `\b` is
an ASCII `\w` boundary and Hangul is not `\w`, so `/\b증량/` matches
nothing — and the clause order is the mirror of the Latin banks, since
Korean closes with the verb. The dose patterns require a target-marking
particle for the same reason the Latin banks require "to"/"by": without
it the permitted restatement "7.5mg을 복용 중이고, 체중은 내려가고
있어요" tripped the lowering pattern.

The Coach and Insights prompts route `ko` down the reviewed English body
plus a Korean reply directive; there is no `safety-contracts.ko.yaml`, so
the native builder has no Korean ground rules to compose from.
2026-08-26 20:00:44 +09:00

124 lines
5.5 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { inviteGrantSchema } from "@/lib/validations/account-sharing";
import {
SETTINGS_DESTINATION_INVENTORY,
isManageDelegateSettingsDestination,
} from "@/lib/record-settings/classification";
/**
* What the product SAYS a manage grant cannot reach agrees with what it cannot
* reach.
*
* Two sentences make that promise, and both were written when it was true:
*
* * the `access` field's description in the sharing request schema, which is
* generated into `docs/api/openapi.yaml` and read by the native client —
* it said MANAGE "never reaches … settings";
* * `recordSharing.unavailable.description`, the paragraph a delegate meets
* on a page sharing does not cover — it said settings "stay with the
* person who owns the account".
*
* v1.37.0 opened one Settings destination to a manage grant, and both
* sentences became false in the most expensive place a sentence can be false:
* one is a consent screen, the other is a published contract. A promise that
* has quietly stopped holding is worse than no promise, because somebody has
* already decided based on it.
*
* So the copy is checked against the classification rather than against
* itself. If a second destination is ever opened, or the anamnesis one is
* closed again, the sentence that describes the boundary has to move with it.
*
* What this cannot check is whether the wording is GOOD. It checks that the
* absolute claim is gone, that the boundary is named the way the code draws it
* (record content versus account configuration), and that all six locales say
* it — which is the part that rots silently.
*/
const ROOT = process.cwd();
const LOCALES = ["de", "en", "es", "fr", "it", "pl", "ko"] as const;
/** The published description of the level, off the Zod schema itself. */
function accessDescription(): string {
const shape = (
inviteGrantSchema as unknown as {
_def: { shape: Record<string, { description?: string }> };
}
)._def.shape;
return shape.access?.description ?? "";
}
function description(locale: string): string {
const bundle = JSON.parse(
readFileSync(join(ROOT, `messages/${locale}.json`), "utf8"),
) as {
recordSharing: { unavailable: { description: string } };
};
return bundle.recordSharing.unavailable.description;
}
describe("the published boundary matches the one the code draws", () => {
const openDestinations = Object.keys(SETTINGS_DESTINATION_INVENTORY).filter(
isManageDelegateSettingsDestination,
);
it("has something to be wrong about", () => {
// The pin every leg below rests on. With no manage-reachable destination
// the old absolute sentence would have been true and this whole file would
// be asserting a style preference.
expect(openDestinations).not.toHaveLength(0);
expect(openDestinations).toContain("anamnesis");
expect(accessDescription().length).toBeGreaterThan(200);
});
it("does not claim a manage grant reaches no settings at all", () => {
const published = accessDescription();
// The exact clause that went stale, and the shape of any replacement for
// it. `settings` may appear — it has to, to say which parts stay with the
// owner — but not as part of a blanket denial.
expect(published).not.toMatch(/never reaches[^.]*\bsettings\b/i);
expect(published).not.toMatch(/no access to settings/i);
});
it("names the boundary the classification actually draws", () => {
// Not a synonym check for its own sake: "record content versus account
// configuration" IS the rule `isManageDelegateSettingsDestination`
// implements, and a description that described some other boundary would
// be a second, disagreeing specification.
expect(accessDescription()).toMatch(
/record content versus account configuration/i,
);
// And the destination that was opened is named, so a reader of the
// contract can tell which part of the record it means.
expect(accessDescription()).toMatch(/allergies, family history/i);
});
it("keeps the refusal paragraph honest in all six locales", () => {
// The delegate-facing half. It is shown on a page sharing does NOT cover,
// so a sentence that over-claims there is read at the exact moment
// somebody is working out where the edge is.
const missing: string[] = [];
for (const locale of LOCALES) {
const text = description(locale);
expect(text.length, locale).toBeGreaterThan(40);
// Every locale has to acknowledge the health background, because that
// is the part the old sentence implicitly denied. Matched on the stem
// rather than the English spelling, and short enough to survive the
// accent: the section is "Anamnèse" in French and "Anamneza" in Polish,
// so anything past "anamn" is already language-specific and a matcher
// tuned to one of them reports the others as broken.
// Korean shares no Latin stem for either concept, so it is matched on
// its own words: "문진" is the anamnesis section, "계정" the account.
if (!/anamn|문진/i.test(text)) missing.push(locale);
// And it must still say the account itself stays with its owner —
// dropping that would fix one over-claim by removing the boundary.
expect(text, locale).toMatch(
/konto|account|compte|cuenta|konta|koncie|계정/i,
);
}
expect(missing).toEqual([]);
});
});