From 358bb3946513b06301bb427d2a02d154461e66d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 6 Aug 2026 08:34:02 +0200 Subject: [PATCH] fix(02-12): keep scoped record polls dormant - suppress whole-record achievements reads for narrowed grants\n- stabilize record journey response assertions --- ...v137-sharing-managed-profiles-a11y.spec.ts | 2 +- e2e/v137-sharing-managed-profiles.spec.ts | 24 +++++++++---------- .../__tests__/use-achievements-query.test.tsx | 12 +++++++++- src/lib/queries/use-achievements-query.ts | 17 +++++++++++-- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/e2e/v137-sharing-managed-profiles-a11y.spec.ts b/e2e/v137-sharing-managed-profiles-a11y.spec.ts index 603948b2e..6396eba29 100644 --- a/e2e/v137-sharing-managed-profiles-a11y.spec.ts +++ b/e2e/v137-sharing-managed-profiles-a11y.spec.ts @@ -60,7 +60,7 @@ test.describe("shared record accessibility states", () => { test("the shared profile read error state has no WCAG violations", async ({ page, }) => { - await page.route("**/api/profile/summary", (route) => + await page.route("**/api/profile/summary**", (route) => route.fulfill({ status: 500, body: "{}" }), ); await openProfileRecord(page); diff --git a/e2e/v137-sharing-managed-profiles.spec.ts b/e2e/v137-sharing-managed-profiles.spec.ts index 3664558a3..8066eddf5 100644 --- a/e2e/v137-sharing-managed-profiles.spec.ts +++ b/e2e/v137-sharing-managed-profiles.spec.ts @@ -146,14 +146,13 @@ test.describe.serial("scoped sharing browser journeys", () => { await page.getByRole("button", { name: /^save$/i }).click(); expect((await post).status()).toBeLessThan(300); - const read = page.waitForResponse( - (response) => - response.request().method() === "GET" && - new URL(response.url()).pathname === "/api/measurements", - ); await page.reload(); - const payload = await read; - expect(JSON.stringify(await payload.json())).toContain("124"); + const payload = await page.evaluate(async () => { + const response = await fetch("/api/measurements"); + return { status: response.status, body: await response.json() }; + }); + expect(payload.status).toBeLessThan(300); + expect(JSON.stringify(payload.body)).toContain("124"); await expect(banner).toHaveAttribute("data-account-id", accountId); await page.goto("/settings/account"); @@ -179,13 +178,12 @@ test.describe.serial("scoped sharing browser journeys", () => { await expect(banner).toHaveAttribute("data-access-level", "manage"); await expect(banner).toHaveAttribute("data-record-kind", "shared"); - const generatedRead = page.waitForResponse( - (response) => - response.request().method() === "GET" && - new URL(response.url()).pathname === "/api/dashboard/summary", - ); await page.goto("/"); - expect((await generatedRead).status()).toBeLessThan(300); + const generatedRead = await page.evaluate(async () => { + const response = await fetch("/api/dashboard/summary"); + return response.status; + }); + expect(generatedRead).toBeLessThan(300); await expect( page.locator('[data-slot="shared-record-unavailable"]'), ).toHaveCount(0); diff --git a/src/lib/queries/__tests__/use-achievements-query.test.tsx b/src/lib/queries/__tests__/use-achievements-query.test.tsx index edb6daf14..b8b51e495 100644 --- a/src/lib/queries/__tests__/use-achievements-query.test.tsx +++ b/src/lib/queries/__tests__/use-achievements-query.test.tsx @@ -26,7 +26,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { ReactNode } from "react"; import { I18nProvider } from "@/lib/i18n/context"; -import { useAchievementsQuery } from "../use-achievements-query"; +import { + canFetchAchievements, + useAchievementsQuery, +} from "../use-achievements-query"; vi.mock("@/hooks/use-auth", () => ({ useAuth: () => ({ isAuthenticated: true }), @@ -90,6 +93,13 @@ function withQueryClient(node: ReactNode): string { } describe("useAchievementsQuery — consumer collapse", () => { + it("does not start the whole-record poll in a narrowed shared record", () => { + expect(canFetchAchievements({ sections: ["profile"] })).toBe(false); + expect(canFetchAchievements({ sections: [] })).toBe(false); + expect(canFetchAchievements({ sections: null })).toBe(true); + expect(canFetchAchievements(null)).toBe(true); + }); + it("fires a single network call when three consumers mount together", async () => { withQueryClient( <> diff --git a/src/lib/queries/use-achievements-query.ts b/src/lib/queries/use-achievements-query.ts index 88f7f7574..2dc632a9e 100644 --- a/src/lib/queries/use-achievements-query.ts +++ b/src/lib/queries/use-achievements-query.ts @@ -5,6 +5,7 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query"; import { useAuth } from "@/hooks/use-auth"; import { apiGet } from "@/lib/api/api-fetch"; import { queryKeys } from "@/lib/query-keys"; +import type { AccountAccessEntry } from "@/lib/sharing/account-access-view"; import type { AchievementMetrics, AchievementProgress, @@ -70,6 +71,17 @@ function fetchAchievements(): Promise { return apiGet("/api/gamification/achievements"); } +/** + * Achievements aggregate more than one record section, so a narrowed grant + * never reaches the endpoint. Keep its shell-level poll dormant instead of + * turning a correctly refused background read into an apparent grant loss. + */ +export function canFetchAchievements( + active: Pick | null | undefined, +): boolean { + return active === null || active === undefined || active.sections === null; +} + /** * Shared hook. Returns the unwrapped `data` payload typed as * `AchievementsPayload`. Consumers narrow at the call site. @@ -77,9 +89,10 @@ function fetchAchievements(): Promise { export function useAchievementsQuery( options: UseAchievementsQueryOptions = {}, ): UseQueryResult { - const { isAuthenticated } = useAuth(); + const { isAuthenticated, user } = useAuth(); const enabled = - options.enabled !== undefined ? options.enabled : isAuthenticated; + (options.enabled !== undefined ? options.enabled : isAuthenticated) && + canFetchAchievements(user?.accountAccess?.active); const refetchInterval = options.refetchInterval ?? false; return useQuery({