Merge remote-tracking branch 'origin/main' into fix/v1397-tz

This commit is contained in:
Marc-André Bombeck committed 2026-10-01 10:59:51 +02:00
commit ada490c7e9
57 files changed
+1485 -99

No files matched your search

@@ -24,6 +24,8 @@ vi.mock("@/hooks/use-auth", () => ({
role: "USER",
timezone: "Europe/Berlin",
disableCoach: authState.disableCoach,
// The server's module map mirrors the record's opt-out.
modules: { coach: !authState.disableCoach },
},
isAuthenticated: true,
isLoading: false,
@@ -34,6 +34,8 @@ vi.mock("@/hooks/use-auth", () => ({
role: "USER",
timezone: "Europe/Berlin",
disableCoach: !coach.available,
// The record's module map, which the section's gate reads.
modules: { coach: coach.available },
},
isAuthenticated: true,
isLoading: false,
@@ -14,6 +14,13 @@ vi.mock("next/navigation", () => ({
usePathname: () => "/settings/coach",
}));
// Past hydration only where a test says so: the gate reads the account once
// mounted, and the pre-hydration pass shows every card.
const mountState = { mounted: false };
vi.mock("@/hooks/use-mounted", () => ({
useMounted: () => mountState.mounted,
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({ data: null, isLoading: false, refetch: vi.fn() }),
useInfiniteQuery: () => ({ data: undefined, isLoading: false }),
@@ -31,10 +38,18 @@ vi.mock("@/hooks/use-ai-capability", () => ({
}),
}));
const authState: { disableCoach: boolean } = { disableCoach: false };
const authState: {
disableCoach: boolean;
modules: { coach?: boolean } | undefined;
} = { disableCoach: false, modules: undefined };
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: { id: "u1", role: "USER", disableCoach: authState.disableCoach },
user: {
id: "u1",
role: "USER",
disableCoach: authState.disableCoach,
modules: authState.modules,
},
isAuthenticated: true,
isLoading: false,
refetch: vi.fn(),
@@ -83,3 +98,24 @@ describe("<CoachSection> — SSR smoke", () => {
expect(html).not.toContain("settings.coach.activate.");
});
});
describe("<CoachSection> — the tuning follows the record, not the account", () => {
it("hides the nudge when the record has the Coach off, whatever the viewer's own opt-out says", () => {
// A guardian with the Coach on, inside a managed record that has it off:
// `/me` publishes the viewer's own `disableCoach` and the RECORD's module
// map. The record's answer decides.
mountState.mounted = true;
authState.disableCoach = false;
authState.modules = { coach: false };
try {
expect(render()).not.toContain('id="coach-nudge"');
authState.disableCoach = true;
authState.modules = { coach: true };
expect(render()).toContain('id="coach-nudge"');
} finally {
mountState.mounted = false;
authState.disableCoach = false;
authState.modules = undefined;
}
});
});
+14 -7
View File
@@ -17,8 +17,12 @@
* 4. Coach memory — durable-fact review + forget controls.
*
* The nav entry is module-gated on `coach`. The preference + nudge cards
* keep their own content gate on `!user.disableCoach`: hiding the Coach hides
* its tuning. The memory (facts, stored conversations and reminders) is never
* keep their own content gate on the same published module state
* (`user.modules.coach`, resolved by the server for the record on screen
* from that record's opt-out and the operator's switches): hiding the Coach
* hides its tuning. The raw `disableCoach` column is the signed-in person's
* own, so inside a managed or shared record it answered for the wrong
* person. The memory (facts, stored conversations and reminders) is never
* gated: it is the person's own record, readable and deletable whatever the
* Coach's state. The reminders card turns read-only while the Coach is
* unavailable and, with the Coach hidden, shows only when rows exist.
@@ -39,6 +43,7 @@ import { CoachNudgeCard } from "@/components/settings/coach-nudge-card";
import { CoachPrefsSection } from "@/components/settings/coach-prefs-section";
import { useAuth } from "@/hooks/use-auth";
import { useMounted } from "@/hooks/use-mounted";
import { isSurfaceVisible } from "@/lib/modules/surface";
export function CoachSection() {
const { isAuthenticated, user } = useAuth();
@@ -47,11 +52,13 @@ export function CoachSection() {
// `useMounted()` to keep the hydration render matching the SSR HTML.
const mounted = useMounted();
const authed = mounted && isAuthenticated;
// The preference + memory cards ride the same content gate as the rest
// of the Coach surface: when the user has hidden the Coach there is no
// assistant to tune or remember anything. (`mounted`-gated so the SSR +
// hydration renders agree.)
const coachEnabled = !mounted || !user?.disableCoach;
// The preference + nudge cards ride the same content gate as the rest of
// the Coach surface: when the record has the Coach hidden there is no
// assistant to tune. Read from the server's module map for the record,
// never recomputed from the account's own columns. (`mounted`-gated so the
// SSR + hydration renders agree.)
const coachEnabled =
!mounted || isSurfaceVisible("settings:coach", user?.modules);
return (
<div className="space-y-6">