test(guards): make path-pinned source reads fail when their subject moves

A test that reads a source file by path and asserts an absence
(`expect(source).not.toMatch(…)`) stays green after a split: the code it
watched moved next door and the old path no longer holds it. A missing file
throws; a file that kept its name but lost its contents does not.

- `readSource(path, anchors)` in helpers/source-files.ts reads one file and
  requires every anchor (a string or pattern the watched code carries) to
  match at least once; it throws on a missing file, a lost anchor, or a read
  without an anchor.
- `readSourceFolder(dir, { floor })` is the folder-scan form for a subject
  that was split into several files.
- The twelve reads that were asserted only negatively now go through
  `readSource` with an anchor: the batch route, the integration-sync
  registrar, the dashboard client, the mood insights client, the sleep
  overview and sleep page, the sidebar, the baseline form, the account
  section (two guards), api-handler's requireAuth and the Google Health sync.
- The two schema parsers the wipe and enum guards share now refuse a parse
  of the real schema that finds fewer than 50 models (and 10 enums), so a
  moved file or a drifted block regex cannot leave those guards comparing
  an allowlist against nothing. Synthetic schemas a test writes stay exempt.

Proof: renaming `setSaving` in the baseline form made the converted
confirm-failure guard throw "the file no longer contains setSaving"; the
helper's own test plants a missing file, a lost string anchor, a lost regex
anchor, an empty anchor list and an under-floor folder, each of which throws.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 04:29:35 +02:00
1 parent c86c7d1a79
commit f3d7ab31e2
16 files changed
+204 -44

No files matched your search

@@ -48,6 +48,7 @@ import {
convertHkValue,
isConvertibleHkUnit,
} from "@/lib/measurements/hk-units";
import { readSource } from "@/__tests__/helpers/source-files";
const ENTRIES = Object.values(APPLE_HEALTH_TYPE_MAP);
@@ -193,10 +194,10 @@ describe("Apple Health mapping — the record's own unit (issue #944)", () => {
"utf8",
);
expect(archive).toContain(optIn);
const batch = readFileSync(
join(process.cwd(), "src/app/api/measurements/batch/route.ts"),
"utf8",
);
const batch = readSource("src/app/api/measurements/batch/route.ts", [
"export const POST",
"hkIdentifier",
]);
expect(
batch,
"the batch route opted into record-unit conversion",
@@ -171,5 +171,16 @@ export function readDeclaredSchema(
};
});
// Every consumer compares this against an allowlist or a "none of these"
// rule, which an empty parse satisfies. For the real schema, this little is
// a broken read (a moved file, a block regex that stopped matching), not a
// schema. A synthetic schema a test writes itself is exempt.
if (schemaPath === SCHEMA_PATH && (models.length < 50 || enums.length < 10)) {
throw new Error(
`readDeclaredSchema(${schemaPath}): parsed ${models.length} model(s) and ` +
`${enums.length} enum(s), below the floor of 50 and 10. Fix the parser ` +
`before trusting a guard built on it.`,
);
}
return { enums, models };
}
+80 -1
View File
@@ -50,7 +50,7 @@
* worktrees, each a full second copy of the tree that would double every
* match. Every caller roots at `src/`, `e2e/` or a subtree of one.
*/
import { readdirSync, readFileSync } from "node:fs";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join, sep } from "node:path";
export function walkSourceFiles(
@@ -229,3 +229,82 @@ export function scanSourceMatches(
options.matchFloor,
) as SourceMatch[];
}
/** Something a file must still contain for a read of it to mean anything. */
export type SourceAnchor = string | RegExp;
function anchorCount(text: string, anchor: SourceAnchor): number {
if (typeof anchor === "string") return text.split(anchor).length - 1;
const flags = anchor.flags.includes("g") ? anchor.flags : `${anchor.flags}g`;
return [...text.matchAll(new RegExp(anchor.source, flags))].length;
}
/**
* Read one source file by path, and prove it is still the file the guard is
* about.
*
* A guard that reads a fixed path and asserts that something is ABSENT
* (`expect(source).not.toMatch(…)`) is green on a file that has been split:
* the code it watched moved next door, the old path holds whatever is left,
* and the absence is true for the wrong reason. A missing file at least
* throws; a file that kept its name but lost its contents does not.
*
* So the caller names at least one anchor: a string or pattern the watched
* code itself carries (the function the guard is about, the call it pins).
* Every anchor must match at least once, or this throws and names the file,
* which is the moment to point the guard at the new home (or at a folder
* with `readSourceFolder`). `rel` is relative to the repository root.
*/
export function readSource(
rel: string,
anchors: SourceAnchor | readonly SourceAnchor[],
): string {
const list = (Array.isArray(anchors) ? anchors : [anchors]) as SourceAnchor[];
if (list.length === 0) {
throw new Error(
`readSource(${rel}): name at least one anchor the watched code carries.`,
);
}
const path = join(process.cwd(), rel);
if (!existsSync(path)) {
throw new Error(
`readSource(${rel}): the file does not exist. If it was split or moved, ` +
`point the guard at the new location or scan the folder with ` +
`readSourceFolder and a floor.`,
);
}
const text = readFileSync(path, "utf8");
const lost = list.filter((anchor) => anchorCount(text, anchor) === 0);
if (lost.length > 0) {
throw new Error(
`readSource(${rel}): the file no longer contains ${lost
.map(String)
.join(", ")}. The code this guard watches has moved; a negative ` +
`assertion on this file would now pass for the wrong reason.`,
);
}
return text;
}
/**
* Read every source file under a folder (relative to the repository root),
* with a floor on how many it found: the folder-scan form of `readSource`
* for a guard whose subject was split into several files. Returns
* `[path relative to the folder, text]` pairs, sorted.
*/
export function readSourceFolder(
relDir: string,
options: {
floor: number;
include?: (rel: string) => boolean;
extensions?: readonly string[];
},
): Array<[string, string]> {
const root = join(process.cwd(), relDir);
const files = walkSourceFiles(root, {
floor: options.floor,
extensions: options.extensions,
}).filter(options.include ?? (() => true));
requireFloor(`readSourceFolder(${relDir})`, files, options.floor);
return files.map((rel) => [rel, readFileSync(join(root, rel), "utf8")]);
}
@@ -38,6 +38,12 @@ export function parseWipeSchema(
const blocks = [...schema.matchAll(/^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm)].map(
(m) => ({ name: m[1], body: m[2] }),
);
if (schemaPath === WIPE_SCHEMA_PATH && blocks.length < 50) {
throw new Error(
`parseWipeSchema(${schemaPath}): parsed ${blocks.length} model(s), below ` +
`the floor of 50. A wipe guard over an empty parse passes on nothing.`,
);
}
const modelNames = new Set(blocks.map((b) => b.name));
const out = new Map<string, ModelShape>();
+4 -3
View File
@@ -22,6 +22,7 @@ import { describe, expect, it } from "vitest";
import { INTEGRATION_CADENCE } from "@/lib/integrations/sync-verdict";
import { INTEGRATION_DISPLAY_NAMES } from "@/components/settings/integrations/integration-fallback-row";
import { readSource } from "@/__tests__/helpers/source-files";
const ROOT = path.resolve(__dirname, "../..");
@@ -65,9 +66,9 @@ describe("moodLog integration stays removed", () => {
});
it("registers no moodlog queue, cron or handler binding", () => {
const registrar = readFileSync(
path.join(ROOT, "src/lib/jobs/reminder/register-integration-sync.ts"),
"utf8",
const registrar = readSource(
"src/lib/jobs/reminder/register-integration-sync.ts",
"export async function registerIntegrationSyncQueues",
);
expect(registrar).not.toContain("moodlog");
expect(registrar).not.toContain("MoodLog");
+55
View File
@@ -0,0 +1,55 @@
/**
* `readSource` and `readSourceFolder`: the reads a path-pinned guard goes
* through, so a split file fails the guard instead of leaving it green.
*/
import { describe, expect, it } from "vitest";
import { readSource, readSourceFolder } from "./helpers/source-files";
const HERE = "src/__tests__/helpers/source-files.ts";
describe("readSource", () => {
it("returns the text when every anchor is present", () => {
const text = readSource(HERE, [
"export function readSource(",
/export function walkSourceFiles\(/,
]);
expect(text.length).toBeGreaterThan(1000);
});
it("throws on a file that does not exist", () => {
expect(() => readSource("src/lib/does-not-exist.ts", "anything")).toThrow(
/does not exist/,
);
});
it("throws when an anchor no longer matches", () => {
expect(() =>
readSource(HERE, ["export function readSource(", "noSuchAnchorHere"]),
).toThrow(/no longer contains noSuchAnchorHere/);
expect(() => readSource(HERE, /export function neverDeclared\(/)).toThrow(
/no longer contains/,
);
});
it("refuses a read without an anchor", () => {
expect(() => readSource(HERE, [])).toThrow(/at least one anchor/);
});
});
describe("readSourceFolder", () => {
it("returns the files with their text, above the floor", () => {
const files = readSourceFolder("src/__tests__/helpers", { floor: 5 });
expect(files.length).toBeGreaterThanOrEqual(5);
expect(files.map(([rel]) => rel)).toContain("source-files.ts");
});
it("throws when the folder holds fewer files than the floor", () => {
expect(() =>
readSourceFolder("src/__tests__/helpers", {
floor: 1,
include: (rel) => rel === "nothing-matches.ts",
}),
).toThrow(/below the stated floor/);
});
});
@@ -19,6 +19,7 @@ import {
import { moduleForMeasurementType } from "@/lib/modules/measurement-scope";
import { locales } from "@/lib/i18n/config";
import type { DataSummary } from "@/lib/analytics/trends";
import { readSource } from "@/__tests__/helpers/source-files";
/**
* The dashboard HRV tile must accept either HRV type.
@@ -250,10 +251,10 @@ describe("the tile the user actually sees", () => {
// current formatting of the replacement, so reformatting cannot break
// it. Freshness is per-type and captioning a live RMSSD tile from the
// SDNN lookup is the failure it guards.
const page = readFileSync(
join(process.cwd(), "src/app/page-client.tsx"),
"utf8",
);
const page = readSource("src/app/page-client.tsx", [
"const tileStaleDays",
"HEART_RATE_VARIABILITY",
]);
expect(page).not.toContain('tileStaleDays("HEART_RATE_VARIABILITY")');
expect(page).not.toContain(
"const hrvSummary = data?.summaries?.HEART_RATE_VARIABILITY",
+4 -5
View File
@@ -40,6 +40,7 @@ import { DELETE } from "../[id]/route";
import { requireAuth } from "@/lib/api-handler";
import { prisma } from "@/lib/db";
import { isApiGloballyEnabled } from "@/lib/app-settings";
import { readSource } from "@/__tests__/helpers/source-files";
const USER = { id: "user-1", username: "u" };
@@ -168,11 +169,9 @@ describe("the switch and the authentication path", () => {
// a token would NOT be live during the outage and hiding the list would
// stop being wrong. This reads the source because the behaviour is an
// absence, and an absence has no call to spy on.
const { readFileSync } = await import("node:fs");
const { join } = await import("node:path");
const source = readFileSync(
join(process.cwd(), "src/lib/api-handler.ts"),
"utf8",
const source = readSource(
"src/lib/api-handler.ts",
"export async function requireAuth(",
);
expect(source).not.toContain("isApiGloballyEnabled");
expect(source).not.toContain("apiGlobal");
@@ -14,6 +14,7 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { describe, expect, it } from "vitest";
import { readSource } from "@/__tests__/helpers/source-files";
const ROOT = process.cwd();
const INSIGHTS = join(ROOT, "src/app/insights");
@@ -52,7 +53,10 @@ describe("Insights add actions", () => {
});
it("the mood page has no text links into logging or the check-in", () => {
const source = readFileSync(join(INSIGHTS, "mood/page-client.tsx"), "utf8");
const source = readSource(
"src/app/insights/mood/page-client.tsx",
"<SubPageAddButton",
);
expect(source).not.toMatch(/<Link\s+href="\/mood"\s+data-slot/);
expect(source).not.toContain('href="/mental-wellbeing"');
expect(source).not.toContain("insights.mood.mentalWellbeingLink");
@@ -11,6 +11,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { isSettingsSectionSlug } from "@/lib/settings/section-slugs";
import { readSource } from "@/__tests__/helpers/source-files";
describe("sleep empty-state CTA href", () => {
it("links to /settings/integrations (a real settings slug)", () => {
@@ -29,9 +30,9 @@ describe("sleep empty-state CTA href", () => {
});
it("never links to the removed /settings/data-sources slug", () => {
const source = readFileSync(
join(process.cwd(), "src/app/insights/sleep/page.tsx"),
"utf8",
const source = readSource(
"src/app/insights/sleep/page.tsx",
"<MetricEmptyState",
);
expect(source).not.toContain("/settings/data-sources");
});
@@ -12,6 +12,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { isSettingsSectionSlug } from "@/lib/settings/section-slugs";
import { readSource } from "@/__tests__/helpers/source-files";
describe("sleep-overview empty-state CTA href", () => {
it("links to /settings/integrations (a real settings slug)", () => {
@@ -30,9 +31,9 @@ describe("sleep-overview empty-state CTA href", () => {
});
it("never links to the non-existent /settings/devices slug", () => {
const source = readFileSync(
join(process.cwd(), "src/components/insights/sleep-overview.tsx"),
"utf8",
const source = readSource(
"src/components/insights/sleep-overview.tsx",
"<EmptyState",
);
expect(source).not.toContain("/settings/devices");
});
@@ -66,6 +66,7 @@ import {
import { ADMIN_SECTIONS } from "@/components/admin/admin-shell";
import { visibleUtilityDestinations } from "../nav-model";
import { delegatedDomains } from "@/lib/sharing/domain-write-support";
import { readSource } from "@/__tests__/helpers/source-files";
function render({
pathname = "/",
@@ -565,9 +566,9 @@ describe("<SidebarNav> carries no rules for a viewport it never shows on", () =>
// A touch screen at most 520 px tall gets the phone shell (the
// `shell-mobile` variant), so the rail is hidden there and a class keyed
// on that query can never apply.
const source = readFileSync(
join(__dirname, "..", "sidebar-nav.tsx"),
"utf8",
const source = readSource(
"src/components/layout/sidebar-nav.tsx",
"export function SidebarNav",
);
expect(source).not.toMatch(/pointer:coarse\)_and_\(height<=520px\)/);
});
@@ -16,8 +16,6 @@
* component tests render server-side and cannot click.
*/
import { describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import {
defaultOnboardingSteps,
@@ -27,6 +25,7 @@ import {
import { runBaselineAttempt } from "../baseline-form";
import { runConfirmFinish } from "../confirm-screen";
import { readSource } from "@/__tests__/helpers/source-files";
type PassableStep = Parameters<
Parameters<typeof runConfirmFinish>[0]["passStep"]
@@ -92,10 +91,10 @@ describe("runBaselineAttempt", () => {
it("is the form's only way to move the pending flag", () => {
// A second, inline `setSaving(false)` is how the flag stopped being
// reset on one of the three exits in the first place.
const source = readFileSync(
join(process.cwd(), "src/components/onboarding/baseline-form.tsx"),
"utf8",
);
const source = readSource("src/components/onboarding/baseline-form.tsx", [
"export function BaselineForm",
"setSaving",
]);
expect(source).not.toContain("setSaving(false)");
expect(source).not.toContain("setSaving(true)");
});
@@ -1,11 +1,10 @@
import type React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { I18nProvider } from "@/lib/i18n/context";
import { AnamnesisSection } from "../anamnesis-section";
import { readSource } from "@/__tests__/helpers/source-files";
/**
* #159 — the "About me" note and the free-text allergies line moved from the
@@ -144,9 +143,9 @@ describe("Anamnese — the About-me note card (#159)", () => {
describe("Account settings — the About-me card is gone (#159)", () => {
it("the account section carries no about-me mount any more", () => {
const source = readFileSync(
join(process.cwd(), "src/components/settings/account-section/index.tsx"),
"utf8",
const source = readSource(
"src/components/settings/account-section/index.tsx",
"export function AccountSection",
);
expect(source).not.toContain("AboutMeSection");
expect(source).not.toContain("about-me-section");
@@ -41,6 +41,7 @@ vi.mock("@/hooks/use-auth", () => ({
import { PasswordCard } from "../password-card";
import { SecuritySection } from "../security-section";
import { readSource } from "@/__tests__/helpers/source-files";
const SLOT = 'data-slot="settings-password-card"';
@@ -108,9 +109,9 @@ describe("change-password card placement", () => {
});
it("left no password form behind in the Account section", () => {
const account = readFileSync(
join(ROOT, "src/components/settings/account-section/index.tsx"),
"utf8",
const account = readSource(
"src/components/settings/account-section/index.tsx",
"export function AccountSection",
);
for (const marker of [
"settings.passwordTitle",
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { readSource } from "@/__tests__/helpers/source-files";
type Provider = "google_health" | "withings";
type UserStatus = "complete" | "partial" | "failed" | "parked" | "skipped";
@@ -45,9 +46,9 @@ const googleHandlerSource = readFileSync(
join(process.cwd(), "src/lib/jobs/reminder/google-health-sync.ts"),
"utf8",
);
const googleSyncSource = readFileSync(
join(process.cwd(), "src/lib/google-health/sync.ts"),
"utf8",
const googleSyncSource = readSource(
"src/lib/google-health/sync.ts",
"export async function syncUserGoogleHealth(",
);
const withingsHandlerSource = readFileSync(
join(process.cwd(), "src/lib/jobs/reminder/withings-sync.ts"),