feat: let the record's module map filter its own navigation

The other half of the switched-payload change, found by the integration
test that asserted the door was gone and got the door. `isNavDestinationVisible`
short-circuited on `sharedRecord`, so a switched session skipped the module
filter entirely and every destination the grant's scope allowed was offered.

That was right while the map was the actor's: a delegate's own preferences
must not hide a domain the record granted. The map is the record's now, so
the objection is gone and the line is what makes a guardian's toggle visible
inside the profile it was set for. Scope decides which doors the grant opens;
the map decides which of them the record uses at all.

The read-only profile summary needs its marker checked against `!sharedRecord`
rather than reached through the removed short-circuit, and the case that
pinned the old reading is turned round with the reason written out.

Refs #939
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 01:09:50 +02:00
1 parent bed2ab568e
commit ccca056f64
3 files changed
+75 -16

No files matched your search

+35 -11
View File
@@ -88,19 +88,43 @@ describe("shared-record navigation", () => {
).toEqual(["/checkups"]);
});
it("uses the active record scope instead of the actor's module flags", () => {
const actorModules = {
medications: false,
labs: false,
mood: false,
mentalHealth: false,
cycle: false,
illness: false,
inboundDocuments: false,
};
/**
* Scope and modules are two filters and they answer different questions.
*
* This case used to assert that the module map was IGNORED inside a shared
* record, and the reason was sound at the time: the map `GET /api/auth/me`
* published was the ACTOR's, which describes their own dashboard, so letting
* it hide a domain the record had granted would have been the actor's
* preference deciding somebody else's navigation.
*
* v1.38.14 (#939) removed the premise rather than the filter. The payload
* now resolves the map for the RECORD the session is inside, so the map is
* the record's own answer to "what does this record track" — and honouring
* it is what makes a guardian's Modules toggle visible inside the profile it
* was set for. The scope still decides which doors the grant opens; the map
* decides which of them the record uses at all.
*/
it("applies the record's own module map on top of its scope", () => {
const scope = ["medications", "labs"] as const;
expect(
visibleNavDestinations(actorModules, true, true, ["medications"]).map(
visibleNavDestinations({}, true, true, [...scope]).map(
(destination) => destination.href,
),
).toEqual(["/medications", "/labs"]);
// The record tracks medications and not labs. Both are in scope; one door
// is offered.
expect(
visibleNavDestinations({ labs: false }, true, true, [...scope]).map(
(destination) => destination.href,
),
).toEqual(["/medications"]);
// And the scope still binds: a module the record tracks is not a door the
// grant opens.
expect(
visibleNavDestinations({}, true, true, ["medications"]).map(
(destination) => destination.href,
),
).toEqual(["/medications"]);
@@ -89,6 +89,32 @@ describe("the destination list under a switch", () => {
});
});
describe("a module the RECORD does not track", () => {
/**
* #939 — the map `GET /api/auth/me` publishes is the RECORD's while a
* session is switched into one, so this filter is what makes a guardian's
* toggle visible. Before it, turning Cycle off for a profile left the Cycle
* door standing in that profile's own navigation, and the toggle wrote a
* column nothing on screen was reading.
*/
it("drops its destination inside the record", () => {
const withCycle = visibleNavDestinations(ALL_MODULES, true, true, null).map(
(d) => d.href,
);
expect(withCycle).toContain("/cycle");
const withoutCycle = visibleNavDestinations(
{ ...ALL_MODULES, cycle: false },
true,
true,
null,
).map((d) => d.href);
expect(withoutCycle).not.toContain("/cycle");
// Only that one door: a module toggle is not a scope change.
expect(withoutCycle).toContain("/measurements");
});
});
describe("the utility tail under a switch", () => {
it("disappears entirely", () => {
// Settings holds credentials, integrations, notification channels and
+14 -5
View File
@@ -342,13 +342,22 @@ function isNavDestinationVisible(
) {
return false;
}
// A shared record's server-resolved scope decides which health-domain doors
// exist. The actor's module preferences describe their own dashboard and
// must not hide a domain the target record explicitly granted.
if (sharedRecord) return true;
if (d.sharedRecordOnly) return false;
// The inverse marker: a destination that exists ONLY inside somebody else's
// record (the read-only profile summary). Guarded on `!sharedRecord` rather
// than reached only through the shared arm, which is what it relied on
// before the module gate below started applying to a shared record too.
if (!sharedRecord && d.sharedRecordOnly) return false;
if (!d.requiresModule) return true;
if (!mounted) return false;
// The module map, and whose it is, is the whole point. A shared record used
// to skip this line: the grant's scope decided which doors existed, and the
// module map was the ACTOR's, which describes their own dashboard and must
// not hide a domain the record granted. `GET /api/auth/me` now resolves the
// map for the RECORD the session is inside (#939), so the objection is gone
// and the line is the one that makes a guardian's toggle visible — without
// it, turning Cycle off for a profile left the Cycle door standing in that
// profile's own navigation. Scope decides which doors the grant opens;
// this decides which of them the record tracks at all.
return modules?.[d.requiresModule] !== false;
}