mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
Merge branch 'fix/v1437-r2-jobs' into release/v1.43.7
This commit is contained in:
commit
e5b7eebe64
62 files changed
+1745
-774
No files matched your search
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -34,10 +34,6 @@ const PEARSON_DEFINITION =
|
||||
/(?:function\s+|const\s+)\w*(?:pearson|correlationCoefficient)\w*\s*(?:=|\()/i;
|
||||
const DELETED = /(?<![A-Za-z])pearsonCorrelation\b/;
|
||||
|
||||
function stripComments(text: string): string {
|
||||
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
||||
}
|
||||
|
||||
function sourceFiles(): string[] {
|
||||
return walkSourceFiles(SRC, { floor: 3000 }).filter(
|
||||
(p) =>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -31,10 +31,6 @@ const CALLERS: Record<string, string> = {
|
||||
|
||||
const CALL = /\breconstructDoseHistory\s*\(/;
|
||||
|
||||
function stripComments(text: string): string {
|
||||
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
||||
}
|
||||
|
||||
describe("one compliance ledger", () => {
|
||||
it("the dose rows are built only where this file says", () => {
|
||||
const callers = walkSourceFiles(SRC, { floor: 3000 })
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* The directories under `src/lib` that hold a provider's client and sync,
|
||||
* derived from the connection keys the product knows (`CONNECTION_KEYS`),
|
||||
* so a guard over provider code cannot keep a list of its own that a new
|
||||
* integration slips past. Each key is checked to be a directory: a key that
|
||||
* names none fails here rather than narrowing a guard in silence.
|
||||
*/
|
||||
import { statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { CONNECTION_KEYS } from "@/lib/integrations/connection-health";
|
||||
|
||||
const LIB = join(process.cwd(), "src", "lib");
|
||||
|
||||
export const PROVIDER_DIRS: readonly string[] = CONNECTION_KEYS.map((key) => {
|
||||
if (!statSync(join(LIB, key), { throwIfNoEntry: false })?.isDirectory()) {
|
||||
throw new Error(`provider-dirs: src/lib/${key} is not a directory`);
|
||||
}
|
||||
return key;
|
||||
});
|
||||
@@ -19,7 +19,7 @@ import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -37,24 +37,15 @@ const ALLOWED: Readonly<Record<string, string>> = {
|
||||
"fills that input field from openInsightsCachedText",
|
||||
};
|
||||
|
||||
/** The legacy name, not its sealed sibling, outside comment lines. */
|
||||
/** The legacy name, not its sealed sibling, outside comments. */
|
||||
const LEGACY_NAME = /\binsightsCachedText\b(?!Encrypted)/;
|
||||
|
||||
function codeLines(source: string): string[] {
|
||||
return source.split("\n").filter((line) => {
|
||||
const t = line.trim();
|
||||
return !(t.startsWith("*") || t.startsWith("//") || t.startsWith("/*"));
|
||||
});
|
||||
}
|
||||
|
||||
function filesSpellingTheLegacyName(): string[] {
|
||||
return walkSourceFiles(SRC, { floor: 2000 })
|
||||
.filter((f) => !f.startsWith("generated/"))
|
||||
.filter((f) => !f.includes("__tests__/") && !/\.test\.tsx?$/.test(f))
|
||||
.filter((f) =>
|
||||
codeLines(readFileSync(join(SRC, f), "utf8")).some((l) =>
|
||||
LEGACY_NAME.test(l),
|
||||
),
|
||||
LEGACY_NAME.test(stripComments(readFileSync(join(SRC, f), "utf8"))),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -417,16 +417,6 @@ describe("a cron-fed queue polls slowly, a sent one does not", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** The text of the first `const NAME = …` in the tree, up to its end. */
|
||||
function constBody(texts: ReadonlyMap<string, string>, name: string): string {
|
||||
for (const [file, raw] of texts) {
|
||||
if (new RegExp(`\\bconst\\s+${name}\\b`).test(raw)) {
|
||||
return bodyOf(texts, { file, fn: name }).split(/\n\n/)[0];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
describe("heavy jobs share the lane", () => {
|
||||
const all = scan(source);
|
||||
const laned = all.bindings.filter((b) => /\bheavyLane\s*\(/.test(b.text));
|
||||
@@ -437,7 +427,10 @@ describe("heavy jobs share the lane", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("holds a lockedPass outside the lane, so an expiry retry does not queue twice", () => {
|
||||
it("holds a lockedPass outside the lane, so an expiry retry finds the run going", () => {
|
||||
// Outside, a retry pg-boss starts while the expired run still holds the
|
||||
// lane finds the lock and settles; inside, the lane would defer it and
|
||||
// run the job a second time once the first is done.
|
||||
const inverted = laned
|
||||
.filter((b) => /\blockedPass\s*\(/.test(b.text))
|
||||
.filter((b) => b.text.indexOf("lockedPass") > b.text.indexOf("heavyLane"))
|
||||
@@ -445,17 +438,45 @@ describe("heavy jobs share the lane", () => {
|
||||
expect(inverted).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a waiter past its expiry only where nothing retries the job", () => {
|
||||
const problems = Object.entries(HEAVY_QUEUES)
|
||||
.filter(([, heavy]) => heavy.oneShot)
|
||||
.flatMap(([queue]) => {
|
||||
const entry = QUEUE_RUNTIME[queue];
|
||||
if (entry?.runtime !== "long") return [`${queue}: not a long queue`];
|
||||
const noRetry = entry.expiryVia.some((v) =>
|
||||
/\bretryLimit:\s*0\b/.test(constBody(source, v)),
|
||||
);
|
||||
return noRetry ? [] : [`${queue}: its send options allow a retry`];
|
||||
});
|
||||
expect(problems).toEqual([]);
|
||||
it("defers only on queues where a job can go back to created", () => {
|
||||
// A deferred job returns from active to created. Under `short` (one
|
||||
// queued job per key) and `stately` (one per state) a twin already
|
||||
// queued would refuse it, and the job would fail instead of waiting.
|
||||
const policies = policiesOf(source);
|
||||
expect(policies.size).toBeGreaterThanOrEqual(40);
|
||||
const refused = Object.keys(HEAVY_QUEUES)
|
||||
.filter((queue) => /^(?:short|stately)$/.test(policies.get(queue) ?? ""))
|
||||
.sort();
|
||||
expect(refused).toEqual([]);
|
||||
// The planted case: the same check over a table that declares one.
|
||||
const planted = new Map(source);
|
||||
const file = REGISTRARS[2];
|
||||
planted.set(
|
||||
file,
|
||||
`const DATA_BACKUP_QUEUE = "data-backup";\nconst queuePolicies: QueuePolicyTable = {\n [DATA_BACKUP_QUEUE]: { policy: "short", reason: "x" },\n};\n`,
|
||||
);
|
||||
expect(policiesOf(planted).get("data-backup")).toBe("short");
|
||||
});
|
||||
});
|
||||
|
||||
/** Every `[QUEUE]: { policy: "…" }` in a registrar's `queuePolicies`. */
|
||||
function policiesOf(texts: ReadonlyMap<string, string>): Map<string, string> {
|
||||
const definitions = definitionsOf(texts);
|
||||
const out = new Map<string, string>();
|
||||
for (const file of REGISTRARS) {
|
||||
const text = code(texts.get(file) ?? "");
|
||||
const m = /const\s+queuePolicies\s*:\s*QueuePolicyTable\s*=\s*\{/.exec(
|
||||
text,
|
||||
);
|
||||
if (!m) continue;
|
||||
const open = m.index + m[0].length - 1;
|
||||
const body = text.slice(open, closeOf(text, open));
|
||||
for (const entry of body.matchAll(
|
||||
/\[\s*([A-Z][A-Z0-9_]*)\s*\]\s*:\s*\{\s*policy\s*:\s*"([a-z]+)"/g,
|
||||
)) {
|
||||
const queue = resolve(definitions, file, entry[1]);
|
||||
if (queue) out.set(queue, entry[2]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -27,17 +27,15 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
/** The shared comment strip, plus SQL line comments inside template strings. */
|
||||
function stripCode(text: string): string {
|
||||
return stripComments(text).replace(/^\s*--.*$/gm, "");
|
||||
}
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
function stripComments(text: string): string {
|
||||
return text
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/^\s*\/\/.*$/gm, "")
|
||||
.replace(/^\s*--.*$/gm, "");
|
||||
}
|
||||
|
||||
/** `DISTINCT ON (type)` or `DISTINCT ON (m."type")`, the type alone. */
|
||||
const LATEST_PER_TYPE = /DISTINCT\s+ON\s*\(\s*(?:\w+\s*\.\s*)?"?type"?\s*\)/i;
|
||||
|
||||
@@ -100,9 +98,7 @@ describe("latest-per-type reads", () => {
|
||||
|
||||
it("no production read keeps one row per type with DISTINCT ON", () => {
|
||||
const offenders = productionFiles().filter((file) =>
|
||||
LATEST_PER_TYPE.test(
|
||||
stripComments(readFileSync(join(SRC, file), "utf8")),
|
||||
),
|
||||
LATEST_PER_TYPE.test(stripCode(readFileSync(join(SRC, file), "utf8"))),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
@@ -152,9 +148,7 @@ describe("latest-per-type reads", () => {
|
||||
|
||||
it("the summaries slice is built only behind its cache cell", () => {
|
||||
const callers = productionFiles().filter((file) =>
|
||||
SLICE_BUILDER_CALL.test(
|
||||
stripComments(readFileSync(join(SRC, file), "utf8")),
|
||||
),
|
||||
SLICE_BUILDER_CALL.test(stripCode(readFileSync(join(SRC, file), "utf8"))),
|
||||
);
|
||||
expect(callers).toEqual(["lib/analytics/summaries-slice.ts"]);
|
||||
});
|
||||
|
||||
@@ -21,7 +21,12 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
/** The shared comment strip, plus SQL line comments inside template strings. */
|
||||
function stripCode(text: string): string {
|
||||
return stripComments(text).replace(/^\s*--.*$/gm, "");
|
||||
}
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -31,13 +36,6 @@ const SINGLE_SHIFT =
|
||||
const DOUBLE_SHIFT =
|
||||
/AT\s+TIME\s+ZONE\s+'UTC'\s*\)?\s*AT\s+TIME\s+ZONE\s+(?:\$\{|\$\d)/i;
|
||||
|
||||
function stripComments(text: string): string {
|
||||
return text
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/^\s*\/\/.*$/gm, "")
|
||||
.replace(/^\s*--.*$/gm, "");
|
||||
}
|
||||
|
||||
describe("naive timestamp zone shifts", () => {
|
||||
it("the matcher fires on the single shift and spares the double one", () => {
|
||||
expect(
|
||||
@@ -62,7 +60,7 @@ describe("naive timestamp zone shifts", () => {
|
||||
!/\.test\.tsx?$/.test(file),
|
||||
);
|
||||
const code = (file: string) =>
|
||||
stripComments(readFileSync(join(SRC, file), "utf8"));
|
||||
stripCode(readFileSync(join(SRC, file), "utf8"));
|
||||
// The reads that shift correctly, so a sweep that reads nothing fails.
|
||||
const doubleShifts = files.filter((file) => DOUBLE_SHIFT.test(code(file)));
|
||||
expect(doubleShifts.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
@@ -11,20 +11,10 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { PROVIDER_DIRS } from "./helpers/provider-dirs";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const LIB = join(process.cwd(), "src", "lib");
|
||||
const PROVIDER_DIRS = [
|
||||
"dawarich",
|
||||
"fitbit",
|
||||
"google-health",
|
||||
"nightscout",
|
||||
"oura",
|
||||
"polar",
|
||||
"strava",
|
||||
"whoop",
|
||||
"withings",
|
||||
];
|
||||
/**
|
||||
* Page loops that stay outside `walkPages` on purpose, with how many matches
|
||||
* each file may hold and why. A file with more matches than its allowance has
|
||||
@@ -58,12 +48,6 @@ const EXCEPTIONS: Readonly<Record<string, { matches: number; why: string }>> = {
|
||||
const PAGE_LOOP =
|
||||
/\b(?:pages?|pageCount|pagesRead|page)(?:\s*-\s*[A-Za-z_]\w*)?\s*(?:<|<=|>|>=)\s*(?:[A-Z_][A-Z0-9_]*|maxPages|[2-9]|[1-9]\d+)\b/g;
|
||||
|
||||
function strip(text: string): string {
|
||||
return text
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
||||
}
|
||||
|
||||
describe("provider clients walk pages through walkPages", () => {
|
||||
const files = walkSourceFiles(LIB, { floor: 1000 }).filter(
|
||||
(f) =>
|
||||
@@ -72,10 +56,20 @@ describe("provider clients walk pages through walkPages", () => {
|
||||
!f.endsWith(".test.ts"),
|
||||
);
|
||||
const matches = (f: string) =>
|
||||
strip(readFileSync(join(LIB, f), "utf8")).match(PAGE_LOOP)?.length ?? 0;
|
||||
stripComments(readFileSync(join(LIB, f), "utf8")).match(PAGE_LOOP)
|
||||
?.length ?? 0;
|
||||
|
||||
it("walks the provider directories", () => {
|
||||
expect(files.length).toBeGreaterThanOrEqual(60);
|
||||
expect(PROVIDER_DIRS).toEqual(
|
||||
expect.arrayContaining(["strava", "whoop", "withings", "google-health"]),
|
||||
);
|
||||
for (const dir of PROVIDER_DIRS) {
|
||||
expect(
|
||||
files.some((f) => f.startsWith(`${dir}/`)),
|
||||
dir,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("finds no page loop of their own beyond the exceptions", () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { walkSourceFiles } from "./helpers/source-files";
|
||||
import { stripComments, walkSourceFiles } from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
@@ -32,14 +32,17 @@ const ALLOWED: Record<string, string> = {
|
||||
"lib/insights/mood-pattern-cards.ts": "A pooled variance for a t statistic.",
|
||||
"lib/insights/derived/trajectory.ts":
|
||||
"The residual standard error of a regression fit (÷ n − 2).",
|
||||
"lib/insights/derived/sleep-score.ts":
|
||||
"A circular SD of the sleep midpoint around its circular mean, the input of the consistency score; `spread` has no circular distance, and the score is never shown as a spread.",
|
||||
};
|
||||
|
||||
/**
|
||||
* A squared deviation: `(x - m) ** 2`, `(x - m) * (x - m)`, or a two-argument
|
||||
* distance squared, `dist(x, m) ** 2` (a circular or otherwise custom
|
||||
* deviation is still a hand-rolled spread).
|
||||
*/
|
||||
const SQUARED_DEVIATION =
|
||||
/\(\s*[\w.[\]]+\s*-\s*[\w.[\]]+\s*\)\s*\*\*\s*2|\(\s*([\w.[\]]+)\s*-\s*([\w.[\]]+)\s*\)\s*\*\s*\(\s*\1\s*-\s*\2\s*\)/;
|
||||
|
||||
function stripComments(text: string): string {
|
||||
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
||||
}
|
||||
/\(\s*[\w.[\]]+\s*-\s*[\w.[\]]+\s*\)\s*\*\*\s*2|\(\s*([\w.[\]]+)\s*-\s*([\w.[\]]+)\s*\)\s*\*\s*\(\s*\1\s*-\s*\2\s*\)|\b[A-Za-z_]\w*\s*\(\s*[\w.[\]]+\s*,\s*[\w.[\]]+\s*\)\s*\*\*\s*2/;
|
||||
|
||||
function computesSpread(code: string): boolean {
|
||||
return SQUARED_DEVIATION.test(code) && code.includes("Math.sqrt(");
|
||||
@@ -59,6 +62,11 @@ describe("one standard deviation", () => {
|
||||
expect(
|
||||
computesSpread("acc += (v - mean) * (v - mean); Math.sqrt(acc / n);"),
|
||||
).toBe(true);
|
||||
expect(
|
||||
computesSpread(
|
||||
"xs.reduce((s, v) => s + circularMinuteDistance(v, mean) ** 2, 0); Math.sqrt(x);",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("no file outside the list computes its own", () => {
|
||||
|
||||
@@ -20,14 +20,36 @@ import { describe, expect, it } from "vitest";
|
||||
const ROOT = join(__dirname, "..", "..");
|
||||
const SRC = join(ROOT, "src");
|
||||
|
||||
/** `file: reason` for every bare clock read that is deliberately left. */
|
||||
const ALLOWED: Record<string, string> = {
|
||||
"src/lib/rollups/mood-rollups.ts": "computed_at is timestamptz",
|
||||
"src/lib/rollups/medication-compliance-rollups.ts":
|
||||
"computed_at is timestamptz",
|
||||
"src/lib/jobs/job-failures.ts": "pg-boss tables are timestamptz",
|
||||
"src/lib/jobs/reminder/registrar-shared.ts": "pg-boss tables are timestamptz",
|
||||
"src/lib/db-compat.ts": "DDL default on a timestamptz column",
|
||||
/**
|
||||
* Every bare clock read that is deliberately left: the file, how many such
|
||||
* reads it holds, and why. The count makes the exception cover those reads
|
||||
* and not the next one written into the same file.
|
||||
*/
|
||||
const ALLOWED: Record<string, { hits: number; why: string }> = {
|
||||
"src/lib/rollups/mood-rollups.ts": {
|
||||
hits: 1,
|
||||
why: "computed_at is timestamptz",
|
||||
},
|
||||
"src/lib/rollups/medication-compliance-rollups.ts": {
|
||||
hits: 1,
|
||||
why: "computed_at is timestamptz",
|
||||
},
|
||||
"src/lib/jobs/job-failures.ts": {
|
||||
hits: 2,
|
||||
why: "pg-boss tables are timestamptz",
|
||||
},
|
||||
"src/lib/jobs/heavy-lane.ts": {
|
||||
hits: 1,
|
||||
why: "pg-boss start_after is timestamptz",
|
||||
},
|
||||
"src/lib/jobs/reminder/registrar-shared.ts": {
|
||||
hits: 1,
|
||||
why: "pg-boss tables are timestamptz",
|
||||
},
|
||||
"src/lib/db-compat.ts": {
|
||||
hits: 1,
|
||||
why: "DDL default on a timestamptz column",
|
||||
},
|
||||
};
|
||||
|
||||
const SQL_CLOCK =
|
||||
@@ -46,11 +68,11 @@ function walk(dir: string, out: string[] = []): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function offenders(): string[] {
|
||||
/** Every bare clock read in the tree, allowed or not. */
|
||||
function bareReads(): string[] {
|
||||
const found: string[] = [];
|
||||
for (const file of walk(SRC)) {
|
||||
const rel = relative(ROOT, file);
|
||||
if (ALLOWED[rel]) continue;
|
||||
readFileSync(file, "utf8")
|
||||
.split("\n")
|
||||
.forEach((line, index) => {
|
||||
@@ -80,18 +102,22 @@ describe("raw SQL clock reads are anchored to UTC", () => {
|
||||
});
|
||||
|
||||
it("leaves no bare NOW() / CURRENT_TIMESTAMP outside the allowlist", () => {
|
||||
expect(offenders()).toEqual([]);
|
||||
const reads = bareReads();
|
||||
const inFile = (rel: string) =>
|
||||
reads.filter((r) => r.startsWith(`${rel}:`)).length;
|
||||
const over = reads.filter((r) => {
|
||||
const rel = r.slice(0, r.indexOf(":"));
|
||||
return !ALLOWED[rel] || inFile(rel) > ALLOWED[rel].hits;
|
||||
});
|
||||
expect(over).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps the allowlist honest: every entry still has a bare read", () => {
|
||||
for (const rel of Object.keys(ALLOWED)) {
|
||||
const lines = readFileSync(join(ROOT, rel), "utf8")
|
||||
.replace(ANCHORED, "")
|
||||
.split("\n");
|
||||
expect(
|
||||
lines.some((line) => SQL_CLOCK.test(line)),
|
||||
rel,
|
||||
).toBe(true);
|
||||
it("keeps the allowlist honest: every entry holds exactly its count", () => {
|
||||
const reads = bareReads();
|
||||
for (const [rel, { hits }] of Object.entries(ALLOWED)) {
|
||||
expect(reads.filter((r) => r.startsWith(`${rel}:`)).length, rel).toBe(
|
||||
hits,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -31,19 +31,24 @@ const REPO_ROOT = resolve(__dirname, "../..");
|
||||
|
||||
/**
|
||||
* Bare formatter sites that are correct as they stand. Keep this short: it is
|
||||
* an inventory of exceptions, not a parking space. Every entry says why.
|
||||
* an inventory of exceptions, not a parking space. Every entry says why, and
|
||||
* how many bare sites the file holds: an exception covers those sites, not
|
||||
* whatever lands in the same file next.
|
||||
*/
|
||||
const ALLOWLIST: ReadonlyArray<{ file: string; why: string }> = [
|
||||
const ALLOWLIST: ReadonlyArray<{ file: string; hits: number; why: string }> = [
|
||||
{
|
||||
file: "src/components/ui/calendar.tsx",
|
||||
hits: 1,
|
||||
why: "A `data-day` attribute on a calendar cell, derived from the grid's own arithmetic. It identifies a cell, it does not render a recorded moment.",
|
||||
},
|
||||
{
|
||||
file: "src/components/cycle/cycle-calendar.tsx",
|
||||
hits: 2,
|
||||
why: "Weekday headers walked from a fixed local date, and a month label built from a month anchor. Both are calendar arithmetic with no instant behind them.",
|
||||
},
|
||||
{
|
||||
file: "src/lib/intl/formatter-cache.ts",
|
||||
hits: 2,
|
||||
why: "The shared cache itself. Its options come from the caller, so the timezone is the caller's to supply and the guard should be asking them, not this.",
|
||||
},
|
||||
];
|
||||
@@ -171,9 +176,13 @@ describe("date formatters name the calendar they render in", () => {
|
||||
});
|
||||
|
||||
it("has no unexplained formatter without an explicit timeZone", () => {
|
||||
const unexplained = hits.filter(
|
||||
(h) => !ALLOWLIST.some((a) => a.file === h.file),
|
||||
);
|
||||
const unexplained = hits.filter((h) => {
|
||||
const entry = ALLOWLIST.find((a) => a.file === h.file);
|
||||
return (
|
||||
!entry ||
|
||||
hits.filter((other) => other.file === h.file).length > entry.hits
|
||||
);
|
||||
});
|
||||
if (unexplained.length > 0) {
|
||||
throw new Error(
|
||||
`${unexplained.length} formatter call(s) render a date without naming a timezone:\n\n` +
|
||||
@@ -182,7 +191,7 @@ describe("date formatters name the calendar they render in", () => {
|
||||
"profile timezone (`readStoredTimezone() || DEFAULT_TIMEZONE` on the client, " +
|
||||
"`resolveUserTimezone(userId)` on the server), or use the shared helpers in " +
|
||||
"@/lib/format. If the value is calendar arithmetic rather than a recorded " +
|
||||
"instant, add the file to ALLOWLIST in this test with the reason.",
|
||||
"instant, add the file to ALLOWLIST in this test with the reason and its count.",
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -209,9 +218,9 @@ describe("date formatters name the calendar they render in", () => {
|
||||
// that has moved on, and it quietly covers whatever lands in that file next.
|
||||
for (const entry of ALLOWLIST) {
|
||||
expect(
|
||||
hits.some((h) => h.file === entry.file),
|
||||
`stale allowlist entry — ${entry.file} no longer has a bare formatter`,
|
||||
).toBe(true);
|
||||
hits.filter((h) => h.file === entry.file).length,
|
||||
`stale allowlist entry — ${entry.file} holds a different number of bare formatters`,
|
||||
).toBe(entry.hits);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user