mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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("),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user