diff --git a/src/components/insights/__tests__/insights-layout-shell-reads.test.tsx b/src/components/insights/__tests__/insights-layout-shell-reads.test.tsx index d50c4da31..8de111cf5 100644 --- a/src/components/insights/__tests__/insights-layout-shell-reads.test.tsx +++ b/src/components/insights/__tests__/insights-layout-shell-reads.test.tsx @@ -13,12 +13,25 @@ import { renderToStaticMarkup } from "react-dom/server"; */ const queries: Array<{ queryKey: readonly unknown[]; enabled?: boolean }> = []; -const stripProps: Array<{ onRegenerate?: unknown }> = []; +const stripProps: Array<{ + onRegenerate?: unknown; + availability?: { summaries?: unknown; hasMood?: boolean }; +}> = []; +/** What the presence read answers, and the record the session is inside. */ +const answer: { presence: unknown; active: unknown } = { + presence: undefined, + active: null, +}; vi.mock("@tanstack/react-query", () => ({ useQuery: (options: { queryKey: readonly unknown[]; enabled?: boolean }) => { queries.push(options); - return { data: undefined, isLoading: false, isError: false }; + const presence = options.queryKey.join("/") === "insights/presence"; + return { + data: presence ? answer.presence : undefined, + isLoading: false, + isError: false, + }; }, useMutation: () => ({ mutate: vi.fn(), isPending: false, data: undefined }), useQueryClient: () => ({ @@ -26,7 +39,10 @@ vi.mock("@tanstack/react-query", () => ({ }), })); vi.mock("@/hooks/use-auth", () => ({ - useAuth: () => ({ isAuthenticated: true, user: { modules: {} } }), + useAuth: () => ({ + isAuthenticated: true, + user: { modules: {}, accountAccess: { active: answer.active } }, + }), })); vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true })); vi.mock("@/hooks/use-ai-capability", () => ({ @@ -41,7 +57,7 @@ vi.mock("@/hooks/use-insights-layout", () => ({ }, })); vi.mock("@/components/insights/insights-tab-strip", () => ({ - InsightsTabStrip: (props: { onRegenerate?: unknown }) => { + InsightsTabStrip: (props: (typeof stripProps)[number]) => { stripProps.push(props); return null; }, @@ -50,7 +66,7 @@ vi.mock("@/components/insights/insights-tab-strip", () => ({ import { queryKeys } from "@/lib/query-keys"; import { InsightsLayoutShell } from "../insights-layout-shell"; -describe("InsightsLayoutShell — what it reads on entry", () => { +describe("InsightsLayoutShell: what it reads on entry", () => { it("keeps the regenerate button and sends no briefing read", () => { renderToStaticMarkup( @@ -82,4 +98,33 @@ describe("InsightsLayoutShell — what it reads on entry", () => { expect(sent).toEqual([JSON.stringify(queryKeys.insightsPresence())]); expect(layoutReads).toEqual([true]); }); + + it("offers no metric pill inside a switched-to record", () => { + // The metric pages read `/api/analytics`, which serves only the caller's + // own record; the mood and medication pills read their own routes. + answer.presence = { + metrics: { WEIGHT: { count: 12 } }, + hasMood: true, + hasMedication: false, + hasWorkouts: false, + hasEcgRecordings: false, + }; + const render = (active: unknown) => { + answer.active = active; + stripProps.length = 0; + renderToStaticMarkup( + +
+ , + ); + return stripProps[0]?.availability; + }; + + expect(render(null)?.summaries).toEqual({ WEIGHT: { count: 12 } }); + const switched = render({ accountId: "record-2" }); + expect(switched?.summaries).toBeUndefined(); + expect(switched?.hasMood).toBe(true); + answer.presence = undefined; + answer.active = null; + }); }); diff --git a/src/components/insights/insights-layout-shell.tsx b/src/components/insights/insights-layout-shell.tsx index 4e0481c73..36f08a00b 100644 --- a/src/components/insights/insights-layout-shell.tsx +++ b/src/components/insights/insights-layout-shell.tsx @@ -72,19 +72,22 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) { // on a first row as well left that page unreachable before data existed. const hasNutrients = user?.modules?.nutrients === true; const hasEcgRecordings = presence?.hasEcgRecordings === true; + // Inside a switched-to record the metric pages read `/api/analytics`, which + // serves only the caller's own record, so their pills would open an error. + const switched = user?.accountAccess?.active != null; // Memoised so an unchanged answer keeps the same reference: the strip is // wrapped in `React.memo` and reruns `buildTabs` on a new object. const availability: InsightInputs | undefined = useMemo(() => { if (!isAuthenticated) return undefined; return { - summaries: presence?.metrics, + summaries: switched ? undefined : presence?.metrics, hasMood: presence?.hasMood === true, hasMedication: presence?.hasMedication === true, hasWorkouts: presence?.hasWorkouts === true, hasNutrients, }; - }, [isAuthenticated, presence, hasNutrients]); + }, [isAuthenticated, presence, hasNutrients, switched]); // v1.15.14 — couple the tab strip to the saved insights layout. // A sub-page pill now shows iff its metric has data AND its slug is