Files
HealthLog/src/__tests__/client-poll-inventory-guard.test.ts
T
Marc-André Bombeck 985372f711 fix(insights): count failed fetches toward a bounded poll's ceiling
The status and advisor polls counted attempts on dataUpdateCount, which a
failed fetch does not move, while the last good payload stayed in place and
still said preparing. A request that kept failing polled an open page for
as long as it stayed open. Attempts now include the failed fetches.

The poll inventory moves from a comment into a guard held to every
refetchInterval in the tree; four polls were missing from the comment.
2026-10-20 10:06:11 +02:00

198 lines
6.3 KiB
TypeScript

/**
* Structural guard: every client poll is in the inventory, in one of the
* three shapes `src/lib/queries/refetch-interval.ts` describes.
*
* The inventory used to be a comment, and a comment cannot fail: three polls
* (the Insights shell's briefing, the medication compliance card and two of
* the status hooks' cards) were missing from it. Here it is data, held to
* every `refetchInterval:` in the tree, with the number each file carries, so
* a new poll in a listed file fails as surely as one in a new file.
*
* A bounded poll counts its attempts with `pollAttemptCount`, failed fetches
* included: on `dataUpdateCount` alone a request that kept failing kept the
* last good payload (still `preparing`) and polled for as long as the page
* stayed open.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
requireFloor,
stripComments,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
type Shape = "standing" | "until-done" | "bounded";
interface Poll {
/** How many `refetchInterval:` the file holds. */
sites: number;
shape: Shape;
/** The interval, and why that figure; or what ends the poll. */
cadence: string;
}
const INVENTORY: Readonly<Record<string, Poll>> = {
// ── Standing: a fixed interval while mounted, paused on a hidden tab ──
"lib/queries/use-dashboard-snapshot.ts": {
sites: 1,
shape: "standing",
cadence:
"120 s, DASHBOARD_REFETCH_INTERVAL_MS; matches the snapshot cache TTL",
},
"lib/queries/use-analytics-query.ts": {
sites: 1,
shape: "standing",
cadence: "120 s, DASHBOARD_REFETCH_INTERVAL_MS",
},
"lib/queries/use-daily-digest.ts": {
sites: 1,
shape: "standing",
cadence: "120 s, DASHBOARD_REFETCH_INTERVAL_MS",
},
"components/settings/notification-status-card.tsx": {
sites: 1,
shape: "standing",
cadence: "30 s; last-delivery results move while the panel is open",
},
"components/admin/app-log-preview-section.tsx": {
sites: 1,
shape: "standing",
cadence: "30 s; an operator watching a live log",
},
"components/gamification/achievement-unlock-notifier.tsx": {
sites: 1,
shape: "standing",
cadence: "5 min; out-of-band unlocks, an app-global poll",
},
"components/admin/host-metrics-chart.tsx": {
sites: 1,
shape: "standing",
cadence: "5 min; the sampler's own cadence",
},
// ── Until done: stops on a terminal state the server always reaches ──
"components/settings/import-panel/apple-health-import-card.tsx": {
sites: 1,
shape: "until-done",
cadence: "2 s until the job reports a terminal status",
},
"components/settings/import-panel/health-connect-import-card.tsx": {
sites: 1,
shape: "until-done",
cadence: "2 s until the job reports a terminal status",
},
"components/admin/backups-section.tsx": {
sites: 1,
shape: "until-done",
cadence: "2 s until no restore is active",
},
"components/documents/documents-view.tsx": {
sites: 1,
shape: "until-done",
cadence:
"4 s until no document is inside its 3-minute upload window without an index",
},
"components/insights/coach-panel/coach-conversation.tsx": {
sites: 1,
shape: "until-done",
cadence:
"2.5 s until the attachment is indexed or its upload window passed",
},
"components/admin/pulse-day-mean-card.tsx": {
sites: 1,
shape: "until-done",
cadence: "10 s until the recompute reports done",
},
"components/admin/measurement-maintenance-card.tsx": {
sites: 1,
shape: "until-done",
cadence: "10 s until the run reports done",
},
"components/insights/period-narrative-card.tsx": {
sites: 1,
shape: "until-done",
cadence:
"5 s until the narrative arrives or `revalidating` clears; only mounted while looked at",
},
// ── Bounded: model text on its way, with an attempt ceiling ──
"hooks/use-insight-status.ts": {
sites: 3,
shape: "bounded",
cadence:
"4 s, nextStatusPollInterval, 12 attempts (metric, biomarker, general cards)",
},
"app/insights/medications/page.tsx": {
sites: 1,
shape: "bounded",
cadence: "4 s, nextStatusPollInterval, 12 attempts (compliance card)",
},
"components/insights/use-insights-advisor.ts": {
sites: 1,
shape: "bounded",
cadence: "25 s, nextAdvisorPollInterval, 10 attempts (advisor)",
},
"components/insights/insights-layout-shell.tsx": {
sites: 1,
shape: "bounded",
cadence: "25 s, nextAdvisorPollInterval, 10 attempts (briefing body)",
},
};
const SITE = /\brefetchInterval\s*:/g;
const files = walkSourceFiles(SRC, { floor: 3000 }).filter(
(f) =>
!f.startsWith("generated/") &&
!f.includes("__tests__/") &&
!/\.test\.tsx?$/.test(f),
);
const code = new Map(
files.map((f) => [f, stripComments(readFileSync(join(SRC, f), "utf8"))]),
);
const sitesIn = (text: string) => text.match(SITE)?.length ?? 0;
describe("client polls", () => {
it("lists every poll in the tree, with the number each file holds", () => {
const found = requireFloor(
"refetchInterval files",
files.filter((f) => sitesIn(code.get(f)!) > 0),
15,
);
const drift = found
.filter((f) => INVENTORY[f]?.sites !== sitesIn(code.get(f)!))
.map((f) => `${f}: ${sitesIn(code.get(f)!)}`);
expect(drift).toEqual([]);
const gone = Object.keys(INVENTORY).filter((f) => !found.includes(f));
expect(gone).toEqual([]);
});
it("counts a bounded poll's attempts with the failed fetches", () => {
const bounded = Object.entries(INVENTORY).filter(
([, p]) => p.shape === "bounded",
);
const problems = bounded.flatMap(([f, p]) => {
const text = code.get(f) ?? "";
const counted =
text.match(/pollAttemptCount\(query\.state\)/g)?.length ?? 0;
return [
...(counted < p.sites
? [`${f}: ${counted} of ${p.sites} counted`]
: []),
...(/\.state\.dataUpdateCount\b/.test(text)
? [`${f}: reads dataUpdateCount alone`]
: []),
];
});
expect(problems).toEqual([]);
});
it("catches an unlisted poll", () => {
expect(sitesIn("useQuery({ refetchInterval: 5_000 })")).toBe(1);
expect(sitesIn("// refetchInterval: 5_000")).toBe(1);
expect(sitesIn(stripComments("// refetchInterval: 5_000"))).toBe(0);
});
});