fix: close four small gaps around the record surface

The privacy dashboard re-implemented the audit-retention resolver with its own
default and its own floor. Two implementations of one disclosure, quoted to the
same person on two pages and by the purge job; it uses the shared resolver now.

The record-content branch in the settings gate matched a classification rather
than a page, so a second manage-writable destination would have rendered the
allergy and family-history managers under its own heading without anybody
choosing that. It resolves through a slug map, and the contract test fails if
the map and the classification disagree in either direction — a destination
cannot join the kind without somebody deciding what it renders.

The managed-profile nav filter assumed a guardian grant is always manage. It is
today, which is why the check was easy to leave out; an entry below that level
would have listed destinations the gate refuses.

Interpolation used a replacement string, and String.replace reads $&, $`, $',
$1 and $$ out of one. A display name containing $' rewrote the sentence around
it — and display names land in exactly the sentences that say who did what in
somebody's health record. A replacer function has no $ grammar at all, which
removes the class rather than escaping the members of it somebody thought of.
This commit is contained in:
Marc-André Bombeck committed 2026-08-07 01:00:02 +02:00
1 parent 5de488c35c
commit 6ae0cc993f
9 files changed
+272 -14

No files matched your search

@@ -8,7 +8,7 @@ const authRef: {
active: {
accountId: string;
recordKind: "managed" | "shared";
level: "manage";
level: "manage" | "write" | "read";
sections: null;
canWrite: true;
} | null;
@@ -38,10 +38,13 @@ vi.mock("../managed-record-settings-section", () => ({
vi.mock("../managed-integration-status", () => ({
ManagedIntegrationStatus: () => <div data-slot="managed-integrations" />,
}));
vi.mock("../record-anamnesis-section", () => ({
RecordAnamnesisSection: () => <div data-slot="record-anamnesis" />,
}));
import { RecordSettingsSectionGate } from "../record-settings-section-gate";
function render(section: "modules" | "integrations") {
function render(section: "modules" | "integrations" | "anamnesis" | "labs") {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<RecordSettingsSectionGate section={section}>
@@ -77,4 +80,83 @@ describe("RecordSettingsSectionGate", () => {
expect(html).toContain("managed-settings");
expect(html).not.toContain("actor-settings");
});
/**
* The record-content destination, and the two ways it must not open.
*
* The branch keys on the SLUG as well as on the classification. Keying on
* the kind alone would render the allergy and family-history managers under
* the heading of any future `manage-writable` destination — silently,
* because a category matched where a page was meant.
*/
describe("the anamnesis destination", () => {
it("opens for an adult manager on an ordinary shared record", () => {
authRef.isLoading = false;
authRef.active = {
accountId: "shared-record",
recordKind: "shared",
level: "manage",
sections: null,
canWrite: true,
};
const html = render("anamnesis");
expect(html).toContain("record-anamnesis");
expect(html).not.toContain("actor-settings");
expect(html).not.toContain("managed-settings");
});
it("opens for a Guardian, rather than the guardian configuration panel", () => {
authRef.isLoading = false;
authRef.active = {
accountId: "managed-record",
recordKind: "managed",
level: "manage",
sections: null,
canWrite: true,
};
const html = render("anamnesis");
expect(html).toContain("record-anamnesis");
expect(html).not.toContain("managed-settings");
});
it("stays shut below MANAGE", () => {
authRef.isLoading = false;
authRef.active = {
accountId: "shared-record",
recordKind: "shared",
level: "write",
sections: null,
canWrite: true,
};
const html = render("anamnesis");
expect(html).not.toContain("record-anamnesis");
expect(html).toContain("shared-record-settings-unavailable-title");
});
it("does not lend its page to another destination of the same kind", () => {
// `labs` is `adult-shared-unavailable` today, so this passes for the
// right reason now; the claim it pins is that the branch asks WHICH
// page, not which category. A second `manage-writable` slug added
// without an arm falls through to the refusal panel.
authRef.isLoading = false;
authRef.active = {
accountId: "shared-record",
recordKind: "shared",
level: "manage",
sections: null,
canWrite: true,
};
const html = render("labs");
expect(html).not.toContain("record-anamnesis");
expect(html).toContain("shared-record-settings-unavailable-title");
});
});
});
@@ -14,6 +14,20 @@ import { RecordAnamnesisSection } from "@/components/settings/record-anamnesis-s
import { SettingsCard } from "@/components/settings/settings-card";
import type { SettingsSectionSlug } from "@/components/settings/section-slugs";
/**
* The record-CONTENT destinations, and what each one renders inside a record.
*
* Keyed by slug so the branch below asks which page it is on rather than which
* category the page belongs to. Every `manage-writable` slug must have an
* entry and nothing else may: the contract test holds both directions, which
* is what stops a new destination inheriting this one's forms.
*/
export const RECORD_CONTENT_SECTIONS: Partial<
Record<SettingsSectionSlug, () => ReactNode>
> = {
anamnesis: RecordAnamnesisSection,
};
const MANAGED_RECORD_SETTINGS_FAMILY_BY_SECTION = {
account: "profile",
modules: "modules",
@@ -65,9 +79,22 @@ export function RecordSettingsSectionGate({
// Checked before the managed branches so a Guardian gets the same record
// surface an adult manager does, rather than the guardian configuration
// panel, which has no family for it.
if (destination.kind === "manage-writable" && level === "manage") {
return <RecordAnamnesisSection />;
}
//
// Resolved through a slug map rather than by matching the kind, and that is
// not belt-and-braces. `manage-writable` is a classification, not a
// component: a second destination joining it — a future record-content page
// — would otherwise render the allergy and family-history managers under its
// own heading, silently, because the branch matched a category where a page
// was meant. `RECORD_CONTENT_SECTIONS` says which page each slug is, a slug
// with no entry falls through to the refusal panel below, and
// `record-settings-contract.test.ts` fails if the map and the classification
// ever disagree — so a destination cannot join the kind without somebody
// deciding what it renders.
const RecordContent =
level === "manage" && destination.kind === "manage-writable"
? RECORD_CONTENT_SECTIONS[section]
: undefined;
if (RecordContent) return <RecordContent />;
if (
recordKind === "managed" &&
+8 -1
View File
@@ -460,10 +460,17 @@ export function SettingsShell({
? allVisibleSections
: allVisibleSections.filter((section) => {
const kind = classifySettingsDestination(section.slug).kind;
// Every destination on either list needs MANAGE, so the level is
// checked once for both record kinds rather than only for the adult
// one. A guardian grant is always MANAGE today, which is exactly why
// the check was easy to leave out — and a managed entry that ever
// arrived below it would have listed destinations the section gate
// refuses, sending somebody to a page that explains it cannot open.
if (activeRecord.level !== "manage") return false;
if (activeRecord.recordKind === "managed") {
return kind === "managed-guardian" || kind === "manage-writable";
}
return kind === "manage-writable" && activeRecord.level === "manage";
return kind === "manage-writable";
});
const activeSection = allVisibleSections.find(
(section) => section.slug === activeSlug,