test(guards): count fixed-path reads through readSourceText in the source-read ratchet

The ratchet on tests that read a source file by path matched readFileSync
and readFile only, so a guard that called readSourceText with a fixed path,
directly or through a local read(rel) wrapper, read the file without the
anchors readSource demands and was never counted. The classifier now counts
both shapes, and leaves alone a readSource call with anchors and a sweep that
feeds the reader from a walk.

The twelve guards that read a fixed file that way now go through readSource
with an anchor each read asserts on, so the baseline stays where it was.
This commit is contained in:
Marc-André Bombeck committed 2026-10-22 19:08:15 +02:00
1 parent 298515d156
commit 760f261a5f
13 files changed
+200 -48

No files matched your search

@@ -16,7 +16,11 @@
import { describe, it, expect } from "vitest";
import { join } from "node:path";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -176,7 +180,7 @@ describe("the cookie-only helpers cannot see the acting account", () => {
const COOKIE_ONLY = ["requireAdmin", "requireCookieAuth", "requireFreshMfa"];
it.each(COOKIE_ONLY)("%s resolves the actor and nothing else", (name) => {
const body = functionBody(read("lib/api-handler.ts"), name);
const body = functionBody(readSource("src/lib/api-handler.ts", name), name);
// Non-zero proof, twice over: the slice found a function, and the slice is
// the function it claims to be. A renamed helper must fail here rather
@@ -190,7 +194,7 @@ describe("the cookie-only helpers cannot see the acting account", () => {
});
it("substitutes a data-scope user in exactly two functions", () => {
const src = read("lib/api-handler.ts");
const src = readSource("src/lib/api-handler.ts", "requireRecordAuth");
const record = functionBody(src, "requireRecordAuth");
expect(record).toContain("readActingCarrier");
@@ -291,7 +295,7 @@ describe("ending a grant and clearing the switch are one act", () => {
it("runs the transition and the cleanup in one transaction", () => {
const body = privateFunctionBody(
read("lib/sharing/grants.ts"),
readSource("src/lib/sharing/grants.ts", "endAndClear"),
"endAndClear",
);
// Non-zero proof: a renamed helper fails here rather than asserting
@@ -308,7 +312,7 @@ describe("ending a grant and clearing the switch are one act", () => {
// claim's own transaction (it takes `tx`), so the guard here is that the
// cleanup is part of the same function as the ending.
const body = functionBody(
read("lib/sharing/grants.ts"),
readSource("src/lib/sharing/grants.ts", "settleHandoverGuardianGrant"),
"settleHandoverGuardianGrant",
);
expect(body.length).toBeGreaterThan(0);
@@ -15,7 +15,11 @@
import { describe, it, expect } from "vitest";
import { join } from "node:path";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -276,7 +280,10 @@ describe("T1 — the Bearer resolution set is frozen", () => {
});
it("the resolver exposes exactly one authorisation arm", () => {
const src = read("lib/auth/bearer.ts");
const src = readSource(
"src/lib/auth/bearer.ts",
'permissions.includes("*")',
);
// One wildcard escape hatch, guarding one deny block. If a second
// `permissions.includes("*")` short-circuit appears, the arm has forked.
const wildcardChecks = src.match(/permissions\.includes\("\*"\)/g) ?? [];
@@ -375,7 +382,10 @@ describe("T3 — the mint sites are frozen", () => {
// does not contradict this: what was retired is a mint at THIS path that
// named no scope and answered for the whole API. A child path minting one
// named scope, listed in `MINT_SITES` above, is the shape that replaced it.
const tokensRoute = read("app/api/tokens/route.ts");
const tokensRoute = readSource(
"src/app/api/tokens/route.ts",
"export const GET",
);
expect(tokensRoute).not.toMatch(/export const POST/);
expect(tokensRoute).toMatch(/export const GET/);
});
@@ -442,12 +452,18 @@ describe("T4b — `documents:write` opens exactly two doors", () => {
]);
// Within the upload file, once: the GET list beside the POST must not
// name it.
const upload = read("app/api/documents/inbound/route.ts");
const upload = readSource(
"src/app/api/documents/inbound/route.ts",
"DOCUMENTS_WRITE_SCOPE",
);
expect(
requireAuthScopes(upload).filter((x) => x === "DOCUMENTS_WRITE_SCOPE"),
).toHaveLength(1);
// And the lookup is a read of one key and nothing else: a single GET.
const lookup = read("app/api/documents/inbound/source/route.ts");
const lookup = readSource(
"src/app/api/documents/inbound/source/route.ts",
"export const GET",
);
expect(
[...lookup.matchAll(/export const (GET|POST|PUT|PATCH|DELETE)\b/g)].map(
(m) => m[1],
@@ -469,7 +485,10 @@ describe("T4c — `workouts:write` opens exactly one door", () => {
);
});
expect(declaring).toEqual(["app/api/workouts/batch/route.ts"]);
const batch = read("app/api/workouts/batch/route.ts");
const batch = readSource(
"src/app/api/workouts/batch/route.ts",
"WORKOUTS_WRITE_SCOPE",
);
expect(
requireAuthScopes(batch).filter((x) => x === "WORKOUTS_WRITE_SCOPE"),
).toHaveLength(1);
@@ -524,7 +543,7 @@ describe("T5 — a delegable route naming a Bearer scope is frozen", () => {
// it and every other leg in this file stays green while a single-purpose
// token gains delegated write — so the refusal is asserted where it lives,
// not only through the behaviour that depends on it.
const src = read("lib/api-handler.ts");
const src = readSource("src/lib/api-handler.ts", "narrow_scope_selector");
expect(src).toMatch(/narrow_scope_selector/);
expect(src).toMatch(/isScopedCredential\(auth\)/);
});
@@ -21,6 +21,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
readSource,
readSourceText,
stripComments,
walkSourceFiles,
@@ -59,8 +60,14 @@ describe("one Pearson correlation", () => {
});
it("both homes still hold their function", () => {
const engine = readSourceText(join(SRC, "lib/insights/correlations.ts"));
const wrapper = readSourceText(join(SRC, "lib/analytics/correlations.ts"));
const engine = readSource(
"src/lib/insights/correlations.ts",
"export function pearson",
);
const wrapper = readSource(
"src/lib/analytics/correlations.ts",
"export function significantPearsonCorrelation",
);
expect(engine).toMatch(/export function pearson\s*\(/);
expect(wrapper).toMatch(
/export function significantPearsonCorrelation\s*\(/,
@@ -24,7 +24,7 @@ import {
mayBeCycleContext,
parse,
} from "./helpers/cycle-verdict-derivation";
import { readSourceText } from "./helpers/source-files";
import { readSource, readSourceText } from "./helpers/source-files";
const ROOT = process.cwd();
@@ -110,19 +110,21 @@ describe("the surfaces that show the verdict actually read it", () => {
// The BBT chart used to walk the calendar itself to find the cycle start,
// which was a second copy of the same walk. It now takes the resolved
// value as a prop, and the cycle page feeds it from the verdict.
const chart = readSourceText(
join(ROOT, "src/components/cycle/bbt-chart.tsx"),
const chart = readSource(
"src/components/cycle/bbt-chart.tsx",
"cycleStartDate",
);
expect(chart).toContain("cycleStartDate");
const paths = accessPaths(parseFile("src/components/cycle/cycle-view.tsx"));
const view = "src/components/cycle/cycle-view.tsx";
const paths = accessPaths(parse(view, readSource(view, "verdict")));
expect([...paths]).toContain("verdict.cycleStartDate");
});
it("the calendar response type declares the verdict as always present", () => {
// An optional verdict is a plausible default that lets a client fall back
// to deriving its own.
const types = readSourceText(join(ROOT, "src/components/cycle/types.ts"));
const types = readSource("src/components/cycle/types.ts", "CycleVerdict");
expect(types).toContain("verdict: CycleVerdict;");
expect(types).not.toContain("verdict?:");
});
@@ -24,7 +24,11 @@
import { describe, it, expect } from "vitest";
import { join } from "node:path";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
const VALIDATOR_MODULE = "@/lib/validations/external-id";
@@ -280,9 +284,9 @@ describe("identities derived outside a Zod schema stay inside the floor", () =>
const b = body("lib/import/csv-measurements.ts");
expect(b).toContain("isStableExternalId");
expect(b).toContain('skip("unstable_external_id")');
expect(read("lib/import/csv-measurements.ts")).toContain(
'| "unstable_external_id"',
);
expect(
readSource("src/lib/import/csv-measurements.ts", "unstable_external_id"),
).toContain('| "unstable_external_id"');
});
it("the MCP write path checks the key BEFORE it is hashed into an identity", () => {
@@ -10,7 +10,11 @@
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -65,7 +69,12 @@ describe("one least-squares line", () => {
// The implementation itself matches, so the sweep read real files.
expect(
HAND_ROLLED_SLOPE.test(
stripComments(readSourceText(join(SRC, "lib/stats/linear-fit.ts"))),
stripComments(
readSource(
"src/lib/stats/linear-fit.ts",
"export function linearFit",
),
),
),
).toBe(true);
});
@@ -22,7 +22,11 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
const MEMORY = "lib/ai/coach/memory";
@@ -59,18 +63,21 @@ function checksBeforeRead(body: string, check: RegExp): boolean {
describe("memory block egress guard", () => {
const block = functionBody(
read(`${MEMORY}/context-block.ts`),
readSource(`src/${MEMORY}/context-block.ts`, "buildMemoryContextBlock"),
"buildMemoryContextBlock",
);
const lines = functionBody(
read(`${MEMORY}/plan-progress.ts`),
readSource(`src/${MEMORY}/plan-progress.ts`, "buildPlanProgressLines"),
"buildPlanProgressLines",
);
it("the memory block checks egress before it reads anything", () => {
expect(block.length).toBeGreaterThan(200);
expect(checksBeforeRead(block, /egressAllowed\(/)).toBe(true);
const helper = read(`${MEMORY}/context-block.ts`);
const helper = readSource(
`src/${MEMORY}/context-block.ts`,
"aiEgressRefusal",
);
expect(helper).toMatch(/aiEgressRefusal\(\s*"coach"/);
expect(helper).toMatch(/aiCapabilityForRecord\(args\.userId, "coach"\)/);
// Broken on purpose: the check moved below the reads.
@@ -16,7 +16,11 @@
import { describe, it, expect } from "vitest";
import { join } from "node:path";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -181,7 +185,10 @@ describe("the recent-proof gate", () => {
});
it("the gate reads the session's own stamps, and only a second factor on an enrolled account", () => {
const src = read("lib/api-handler.ts");
const src = readSource(
"src/lib/api-handler.ts",
"export async function assertRecentCookieProof(",
);
const start = src.indexOf("export async function assertRecentCookieProof(");
expect(start).toBeGreaterThan(-1);
const body = src.slice(start, src.indexOf("\nexport ", start + 1));
@@ -41,7 +41,11 @@
import { describe, it, expect } from "vitest";
import { join } from "node:path";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -80,7 +84,10 @@ function functionBody(src: string, name: string): string {
return "";
}
const API_HANDLER = read("lib/api-handler.ts");
const API_HANDLER = readSource("src/lib/api-handler.ts", [
"requireRecordAuth",
"requireGuardianAuth",
]);
/** The two helpers that resolve a record and therefore must consult the fence. */
const FENCED_RESOLVERS = ["requireRecordAuth", "requireGuardianAuth"] as const;
@@ -180,8 +187,11 @@ describe("the fence runs in exactly two auth helpers", () => {
// client's context is unknown; the switch endpoint is the way out of a
// context that cannot be proved. Fencing either would make an unprovable
// context unrecoverable.
const me = read("app/api/auth/me/route.ts");
const sw = read("app/api/account/switch/route.ts");
const me = readSource("src/app/api/auth/me/route.ts", "requireActorAuth");
const sw = readSource(
"src/app/api/account/switch/route.ts",
"requireActorAuth",
);
expect(me).toContain("requireActorAuth");
expect(sw).toContain("requireActorAuth");
expect(me).not.toContain("assertRecordSessionFence");
@@ -228,7 +238,10 @@ describe("the epoch has one writer and a closed reader set", () => {
});
it("is consulted by the idempotency wrapper, above the cache", () => {
const src = read("lib/idempotency.ts");
const src = readSource(
"src/lib/idempotency.ts",
"refuseStaleRecordSessionClaim(claim)",
);
const fence = src.indexOf("refuseStaleRecordSessionClaim(claim)");
const cache = src.indexOf("findCached(ctx)");
const claimRow = src.indexOf("claimKey(ctx)");
@@ -20,6 +20,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
readSource,
readSourceText,
stripComments,
walkSourceFiles,
@@ -87,10 +88,17 @@ describe("one standard deviation", () => {
});
it("the series API and the AI and MCP payload report the same spread", () => {
expect(read("app/api/measurements/series/route.ts")).toMatch(
/standardDeviation\(\s*values,\s*READING_SPREAD\s*\)/,
expect(
stripComments(
readSource(
"src/app/api/measurements/series/route.ts",
"READING_SPREAD",
),
),
).toMatch(/standardDeviation\(\s*values,\s*READING_SPREAD\s*\)/);
const features = stripComments(
readSource("src/lib/insights/features.ts", "READING_SPREAD"),
);
const features = read("lib/insights/features.ts");
expect(
features.match(/roundedSpread\(\s*vals,\s*READING_SPREAD\s*\)/g),
).toHaveLength(2);
+2 -2
View File
@@ -16,7 +16,7 @@
import { readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
import { describe, expect, it } from "vitest";
import { readSourceText } from "./helpers/source-files";
import { readSource, readSourceText } from "./helpers/source-files";
const ROOT = join(__dirname, "..", "..");
const SRC = join(ROOT, "src");
@@ -85,7 +85,7 @@ function bareReads(): string[] {
describe("raw SQL clock reads are anchored to UTC", () => {
it("finds the anchored form across the swept files", () => {
// The matcher must see something, or a broken walk would pass.
const passkey = readSourceText(join(SRC, "lib/auth/passkey.ts"));
const passkey = readSource("src/lib/auth/passkey.ts", ANCHORED);
expect(passkey.match(ANCHORED)?.length).toBeGreaterThanOrEqual(3);
expect(SQL_CLOCK.test("AND expires_at > NOW()")).toBe(true);
expect(SQL_CLOCK.test("updated_at = now()")).toBe(true);
+21 -5
View File
@@ -25,7 +25,11 @@
import { describe, it, expect } from "vitest";
import { join } from "node:path";
import { readSourceText, walkSourceFiles } from "./helpers/source-files";
import {
readSource,
readSourceText,
walkSourceFiles,
} from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
@@ -256,7 +260,10 @@ describe("S6 — the erasure routes accept a fresh-factor elevation", () => {
// A password-proved elevation must not erase an account, and the gate
// must not grow a second, inline factor check beside the elevation: one
// mint surface, one redemption path.
const src = read("lib/api-handler.ts");
const src = readSource(
"src/lib/api-handler.ts",
"export async function requireFreshMfaOrElevationIfEnrolled(",
);
const start = src.indexOf(
"export async function requireFreshMfaOrElevationIfEnrolled(",
);
@@ -307,7 +314,10 @@ describe("S2 — mint and redeem happen in exactly one place each", () => {
describe("S3 — requireAdmin stays cookie-only", () => {
it("resolves through getSession and nothing else", () => {
const src = read("lib/api-handler.ts");
const src = readSource("src/lib/api-handler.ts", [
"export async function requireAdmin(",
"export async function requireCookieAuth(",
]);
const body = src.slice(
src.indexOf("export async function requireAdmin("),
src.indexOf("export async function requireCookieAuth("),
@@ -335,7 +345,10 @@ describe("S4 — the mint surface refuses a cookie", () => {
});
it("requireBearerAuth never consults the session cookie", () => {
const src = read("lib/api-handler.ts");
const src = readSource("src/lib/api-handler.ts", [
"export async function requireBearerAuth(",
"export const STEP_UP_ELEVATION_HEADER",
]);
const body = src.slice(
src.indexOf("export async function requireBearerAuth("),
src.indexOf("export const STEP_UP_ELEVATION_HEADER"),
@@ -372,7 +385,10 @@ describe("S5 — a password proof cannot satisfy a fresh-factor gate", () => {
});
it("the claim re-checks the factor rather than trusting the validation", () => {
const src = read("lib/auth/step-up.ts");
const src = readSource(
"src/lib/auth/step-up.ts",
"export async function claimStepUpElevation(",
);
const claim = src.slice(
src.indexOf("export async function claimStepUpElevation("),
);
+58 -2
View File
@@ -19,6 +19,9 @@
* assertion then passes for the wrong reason. New ones go through
* `readSource(path, anchors)` or `readSourceFolder(dir, { floor })` in
* `helpers/source-files.ts`, which this count does not include.
* `readSourceText` is the unanchored reader the sweeps use; called with a
* fixed source path, directly or through a local wrapper that is, it is
* the same read without the anchors, and it counts.
*
* A new offender fails; one that disappears (or loses casts) fails until
* the lower number is written back with
@@ -29,7 +32,10 @@
* the Prisma client entry itself, or a spy on every delegate, is not a
* pattern in use and is not matched. The source-read check is a text
* heuristic: a file that calls `readFileSync`/`readFile` and names a source
* path (a literal ending in `.ts`, `.tsx`, `.js` or `.mjs`).
* path (a literal ending in `.ts`, `.tsx`, `.js` or `.mjs`), or that calls
* `readSourceText` with such a literal, or calls a local one-parameter wrapper
* of `readSourceText` with one. A wrapper fed from a list of paths is not
* matched.
*/
import { describe, expect, it } from "vitest";
import ts from "typescript";
@@ -45,6 +51,26 @@ const DB_MOCK =
const HANDLER_STUB = /vi\.(?:do)?[mM]ock\(\s*["'`]@\/lib\/api-handler["'`]/;
const READS_FILE = /\b(?:readFileSync|readFile)\s*\(/;
const SOURCE_PATH = /["'`][^"'`\n]*\.(?:tsx?|mjs|js)["'`]/;
/** `readSourceText(...)` whose argument names a source path literal. */
const READ_SOURCE_TEXT_FIXED = new RegExp(
String.raw`\breadSourceText\(\s*(?:join\([^)]*?)?` + SOURCE_PATH.source,
);
/** A local wrapper whose one parameter becomes the `readSourceText` path. */
const READ_SOURCE_TEXT_WRAPPER =
/(?:function\s+(\w+)|(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?)\s*\(\s*(\w+)\s*(?::\s*string)?[^)]*\)[^{=]*(?:=>|\{)[\s\S]{0,160}?readSourceText\(\s*(?:join\([^)]*?\b\3\b|\3\b)/g;
/** Whether a test reads a fixed source path through `readSourceText`. */
function readsSourceTextByPath(text: string): boolean {
if (!text.includes("readSourceText")) return false;
if (READ_SOURCE_TEXT_FIXED.test(text)) return true;
for (const m of text.matchAll(READ_SOURCE_TEXT_WRAPPER)) {
const name = m[1] ?? m[2];
if (new RegExp(`\\b${name}\\(\\s*` + SOURCE_PATH.source).test(text)) {
return true;
}
}
return false;
}
function asNeverCount(rel: string, text: string): number {
if (!text.includes("as never")) return 0;
@@ -68,7 +94,9 @@ function classifyTest(rel: string, text: string) {
dbMock: routeTest && DB_MOCK.test(text),
handlerStub: routeTest && HANDLER_STUB.test(text),
asNever: asNeverCount(rel, text),
sourceRead: READS_FILE.test(text) && SOURCE_PATH.test(text),
sourceRead:
(READS_FILE.test(text) && SOURCE_PATH.test(text)) ||
readsSourceTextByPath(text),
};
}
@@ -143,6 +171,8 @@ describe("test realism ratchet", () => {
});
const READ_CALL = ["read", "File", "Sync"].join("");
// Split for the same reason: this file must not match its own pattern.
const READ_TEXT = ["read", "Source", "Text"].join("");
describe("the classifier", () => {
it("sees the shapes it counts", () => {
@@ -182,6 +212,32 @@ describe("the classifier", () => {
});
});
it("counts a fixed path read through the unanchored reader", () => {
const direct = `const src = ${READ_TEXT}(join(SRC, "lib/x.ts"));`;
const wrapped = [
`function read(rel: string): string {`,
` return ${READ_TEXT}(join(SRC, rel));`,
`}`,
`const src = read("lib/api-handler.ts");`,
].join("\n");
expect(classifyTest("__tests__/a.test.ts", direct).sourceRead).toBe(true);
expect(classifyTest("__tests__/a.test.ts", wrapped).sourceRead).toBe(true);
});
it("leaves an anchored read and a sweep through the reader alone", () => {
const anchored = 'const src = readSource("src/lib/x.ts", "anchor");';
const sweep = [
`function read(rel: string): string {`,
` return ${READ_TEXT}(join(SRC, rel));`,
`}`,
`for (const rel of walkSourceFiles(SRC, { floor: 10 })) read(rel);`,
].join("\n");
expect(classifyTest("__tests__/a.test.ts", anchored).sourceRead).toBe(
false,
);
expect(classifyTest("__tests__/a.test.ts", sweep).sourceRead).toBe(false);
});
it("counts a relative database mock and only under app/api", () => {
expect(
classifyTest(