diff --git a/src/__tests__/acting-account-boundary-guard.test.ts b/src/__tests__/acting-account-boundary-guard.test.ts index 5a3dc2bef..ec8ebafb7 100644 --- a/src/__tests__/acting-account-boundary-guard.test.ts +++ b/src/__tests__/acting-account-boundary-guard.test.ts @@ -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); diff --git a/src/__tests__/bearer-scope-enforcement-guard.test.ts b/src/__tests__/bearer-scope-enforcement-guard.test.ts index 21ab2d10a..8f38b2dc1 100644 --- a/src/__tests__/bearer-scope-enforcement-guard.test.ts +++ b/src/__tests__/bearer-scope-enforcement-guard.test.ts @@ -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\)/); }); diff --git a/src/__tests__/correlation-single-implementation-guard.test.ts b/src/__tests__/correlation-single-implementation-guard.test.ts index c796e97d6..6a6b1d808 100644 --- a/src/__tests__/correlation-single-implementation-guard.test.ts +++ b/src/__tests__/correlation-single-implementation-guard.test.ts @@ -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*\(/, diff --git a/src/__tests__/cycle-verdict-single-decider-guard.test.ts b/src/__tests__/cycle-verdict-single-decider-guard.test.ts index 48a19d200..fe0d9101e 100644 --- a/src/__tests__/cycle-verdict-single-decider-guard.test.ts +++ b/src/__tests__/cycle-verdict-single-decider-guard.test.ts @@ -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?:"); }); diff --git a/src/__tests__/external-id-stability-guard.test.ts b/src/__tests__/external-id-stability-guard.test.ts index 09b03fc17..3d3d27425 100644 --- a/src/__tests__/external-id-stability-guard.test.ts +++ b/src/__tests__/external-id-stability-guard.test.ts @@ -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", () => { diff --git a/src/__tests__/linear-fit-single-implementation-guard.test.ts b/src/__tests__/linear-fit-single-implementation-guard.test.ts index ca5b7888a..2f149383e 100644 --- a/src/__tests__/linear-fit-single-implementation-guard.test.ts +++ b/src/__tests__/linear-fit-single-implementation-guard.test.ts @@ -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); }); diff --git a/src/__tests__/memory-block-egress-guard.test.ts b/src/__tests__/memory-block-egress-guard.test.ts index 7c0fa8bd7..22ae40339 100644 --- a/src/__tests__/memory-block-egress-guard.test.ts +++ b/src/__tests__/memory-block-egress-guard.test.ts @@ -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. diff --git a/src/__tests__/recent-proof-surface-guard.test.ts b/src/__tests__/recent-proof-surface-guard.test.ts index 9e1838b78..4b0acd796 100644 --- a/src/__tests__/recent-proof-surface-guard.test.ts +++ b/src/__tests__/recent-proof-surface-guard.test.ts @@ -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)); diff --git a/src/__tests__/record-session-fence-guard.test.ts b/src/__tests__/record-session-fence-guard.test.ts index e6cacd6a0..eed7928bc 100644 --- a/src/__tests__/record-session-fence-guard.test.ts +++ b/src/__tests__/record-session-fence-guard.test.ts @@ -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)"); diff --git a/src/__tests__/spread-single-implementation-guard.test.ts b/src/__tests__/spread-single-implementation-guard.test.ts index 815f3cb9c..434e49f7d 100644 --- a/src/__tests__/spread-single-implementation-guard.test.ts +++ b/src/__tests__/spread-single-implementation-guard.test.ts @@ -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); diff --git a/src/__tests__/sql-now-utc-guard.test.ts b/src/__tests__/sql-now-utc-guard.test.ts index 096f85055..ce1f2e423 100644 --- a/src/__tests__/sql-now-utc-guard.test.ts +++ b/src/__tests__/sql-now-utc-guard.test.ts @@ -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); diff --git a/src/__tests__/step-up-elevation-guard.test.ts b/src/__tests__/step-up-elevation-guard.test.ts index e295dba3b..250f9bb83 100644 --- a/src/__tests__/step-up-elevation-guard.test.ts +++ b/src/__tests__/step-up-elevation-guard.test.ts @@ -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("), ); diff --git a/src/__tests__/test-realism-ratchet.test.ts b/src/__tests__/test-realism-ratchet.test.ts index e35e78a9c..9fbd4c0dc 100644 --- a/src/__tests__/test-realism-ratchet.test.ts +++ b/src/__tests__/test-realism-ratchet.test.ts @@ -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(