diff --git a/src/__tests__/coach-usage-owner-columns-guard.test.ts b/src/__tests__/coach-usage-owner-columns-guard.test.ts index 7b12a1a95..4a405d812 100644 --- a/src/__tests__/coach-usage-owner-columns-guard.test.ts +++ b/src/__tests__/coach-usage-owner-columns-guard.test.ts @@ -98,12 +98,14 @@ describe("coach_usage — the two counters move together", () => { it("names operator_tokens in every clause that names total_tokens", () => { const offences: string[] = []; + let examined = 0; for (const file of files) { const source = readFileSync(file, "utf8"); if (!source.includes("coach_usage")) continue; for (const literal of sqlLiterals(source)) { for (const clause of columnClauses(literal)) { if (!clause.text.includes("total_tokens")) continue; + examined += 1; if (clause.text.includes("operator_tokens")) continue; offences.push( `${relative(REPO_ROOT, file)} — ${clause.label}: ${clause.text @@ -118,6 +120,8 @@ describe("coach_usage — the two counters move together", () => { offences, `Every raw coach_usage clause that touches total_tokens must touch operator_tokens too:\n${offences.join("\n")}`, ).toEqual([]); + // No clauses examined would agree with the rule above on nothing. + expect(examined).toBeGreaterThanOrEqual(6); }); it("keeps the raw-SQL writer set to the files that reason about the ledger", () => { diff --git a/src/__tests__/helpers/source-files.ts b/src/__tests__/helpers/source-files.ts index 9a2a3cff1..15a5da508 100644 --- a/src/__tests__/helpers/source-files.ts +++ b/src/__tests__/helpers/source-files.ts @@ -50,8 +50,8 @@ * 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 } from "node:fs"; -import { sep } from "node:path"; +import { readdirSync, readFileSync } from "node:fs"; +import { join, sep } from "node:path"; export function walkSourceFiles( root: string, @@ -140,3 +140,92 @@ export function stripComments(source: string): string { return lines.join("\n"); } + +/** + * Asserts that a sweep found at least `floor` things, and returns them. + * + * `walkSourceFiles` puts a floor under the walk. This puts one under what a + * guard does with it: the narrowed file set, the call sites a matcher picked + * out, the routes an inventory extracted. A matcher that drifts out of step + * with the code — a renamed helper, a call split across two lines, a regex + * that stopped compiling the way it was meant to — returns an empty list, and + * an empty list agrees with every allowlist and every "none of these may + * exist" rule. That is how the Bearer-scope guard stayed green for weeks while + * matching nothing. + * + * Pin `floor` at the real count when the set is a closed inventory, and below + * it with headroom only when the count legitimately moves. A floor of zero is + * refused: it is the bug this exists to prevent, spelled as a parameter. + */ +export function requireFloor( + label: string, + found: readonly T[], + floor: number, +): readonly T[] { + if (!Number.isInteger(floor) || floor < 1) { + throw new Error( + `requireFloor(${label}): floor must be a positive integer, got ${floor}.`, + ); + } + if (found.length < floor) { + throw new Error( + `${label}: found ${found.length}, below the stated floor of ${floor}. ` + + `A matcher that finds this little has drifted from the code it ` + + `reads; fix the matcher before lowering the floor.`, + ); + } + return found; +} + +export interface SourceMatch { + /** Path relative to the scanned root, posix-separated. */ + file: string; + /** 1-based line of the match start. */ + line: number; + /** The matched text. */ + text: string; +} + +/** + * Walk `root`, run `pattern` over every kept file, and return each match — + * with a floor on the files read AND a floor on the matches found. + * + * `pattern` must carry the `g` flag; it is the only way to report more than + * one hit per file, and a non-global regex silently finding one hit per file + * is the kind of quiet narrowing this helper is for. + */ +export function scanSourceMatches( + root: string, + pattern: RegExp, + options: { + fileFloor: number; + matchFloor: number; + extensions?: readonly string[]; + include?: (rel: string) => boolean; + stripComments?: boolean; + }, +): SourceMatch[] { + if (!pattern.global) { + throw new Error(`scanSourceMatches: pattern ${pattern} needs the g flag.`); + } + const files = walkSourceFiles(root, { + floor: options.fileFloor, + extensions: options.extensions, + }).filter(options.include ?? (() => true)); + requireFloor(`scanSourceMatches(${root}) files`, files, options.fileFloor); + + const matches: SourceMatch[] = []; + for (const file of files) { + const raw = readFileSync(join(root, file), "utf8"); + const source = options.stripComments ? stripComments(raw) : raw; + for (const m of source.matchAll(pattern)) { + const line = source.slice(0, m.index).split("\n").length; + matches.push({ file, line, text: m[0] }); + } + } + return requireFloor( + `scanSourceMatches(${root}, ${pattern})`, + matches, + options.matchFloor, + ) as SourceMatch[]; +} diff --git a/src/__tests__/measurement-reference-index-guard.test.ts b/src/__tests__/measurement-reference-index-guard.test.ts index ebf7c28db..4cb727aa6 100644 --- a/src/__tests__/measurement-reference-index-guard.test.ts +++ b/src/__tests__/measurement-reference-index-guard.test.ts @@ -64,6 +64,7 @@ describe("columns that point into measurements are indexed", () => { const refs = measurementReferences(); it("finds the references (a matcher that finds none proves nothing)", () => { + expect(refs.length).toBeGreaterThanOrEqual(2); expect(refs).toEqual( expect.arrayContaining([ "ecg_recordings.measurement_id", diff --git a/src/components/insights/__tests__/coach-metric-scope-inventory.test.ts b/src/components/insights/__tests__/coach-metric-scope-inventory.test.ts index 34f8afb5a..573d475c2 100644 --- a/src/components/insights/__tests__/coach-metric-scope-inventory.test.ts +++ b/src/components/insights/__tests__/coach-metric-scope-inventory.test.ts @@ -4,6 +4,7 @@ import { join, resolve } from "node:path"; import { describe, expect, it } from "vitest"; import type { CoachScopeSource } from "@/lib/ai/coach/types"; +import { requireFloor } from "@/__tests__/helpers/source-files"; import { scopeSourceFromMetricKey, scopeSourceMetricLabelKey, @@ -38,8 +39,10 @@ function sourceFilesBelow(directory: string): string[] { } function genericMetricCardInventory(): Set { - const insightRouteSources = sourceFilesBelow( - join(PROJECT_ROOT, "src/app/insights"), + const insightRouteSources = requireFloor( + "insight route sources", + sourceFilesBelow(join(PROJECT_ROOT, "src/app/insights")), + 60, ).map((path) => readFileSync(path, "utf8")); const recoverySource = readFileSync( join(PROJECT_ROOT, "src/components/insights/recovery/recovery-section.tsx"), diff --git a/src/components/settings/__tests__/one-piece-anatomy.test.tsx b/src/components/settings/__tests__/one-piece-anatomy.test.tsx index d801cf1e7..48104c044 100644 --- a/src/components/settings/__tests__/one-piece-anatomy.test.tsx +++ b/src/components/settings/__tests__/one-piece-anatomy.test.tsx @@ -9,6 +9,7 @@ import { PageHeader } from "@/components/ui/page-header"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { SettingsCardActions } from "@/components/settings/_card-actions"; +import { requireFloor } from "@/__tests__/helpers/source-files"; /** * Settings + Admin read as one surface. These pin the parts of that claim @@ -179,8 +180,12 @@ describe("the Settings and Admin shells route their heading through PageHeader", describe("the abolished body gutter stays abolished", () => { it("has no `pl-7` left under Settings or Admin", () => { const offenders: string[] = []; - for (const dir of ["settings", "admin"]) { - for (const file of walk(join(ROOT, "src", "components", dir))) { + for (const [dir, floor] of [ + ["settings", 130], + ["admin", 30], + ] as const) { + const files = walk(join(ROOT, "src", "components", dir)); + for (const file of requireFloor(`${dir} components`, files, floor)) { const source = readFileSync(file, "utf8"); for (const [i, line] of source.split("\n").entries()) { // Comments may name the class they retired; class strings may not. diff --git a/src/components/settings/__tests__/settings-card-header-coverage.test.ts b/src/components/settings/__tests__/settings-card-header-coverage.test.ts index 6e3d1bfa6..1a9b86748 100644 --- a/src/components/settings/__tests__/settings-card-header-coverage.test.ts +++ b/src/components/settings/__tests__/settings-card-header-coverage.test.ts @@ -23,6 +23,8 @@ import { describe, it, expect } from "vitest"; import { readFileSync, readdirSync, statSync } from "node:fs"; import { join, relative } from "node:path"; +import { requireFloor } from "@/__tests__/helpers/source-files"; + const ROOT = join(__dirname, "../../../.."); const SETTINGS_DIR = join(ROOT, "src/components/settings"); @@ -59,7 +61,8 @@ describe("Settings card headers route through SettingsCardHeader", () => { it("has no hand-rolled

outside the primitive + allowlist", () => { const offenders: string[] = []; - for (const file of walk(SETTINGS_DIR)) { + const files = requireFloor("settings cards", walk(SETTINGS_DIR), 80); + for (const file of files) { const base = file.slice(file.lastIndexOf("/") + 1); if (base === PRIMITIVE) continue; if (ALLOWLIST.has(base)) continue; diff --git a/src/components/settings/integrations/__tests__/pill-mapper.test.tsx b/src/components/settings/integrations/__tests__/pill-mapper.test.tsx index 28b3e013c..f67be81d0 100644 --- a/src/components/settings/integrations/__tests__/pill-mapper.test.tsx +++ b/src/components/settings/integrations/__tests__/pill-mapper.test.tsx @@ -9,8 +9,9 @@ */ import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; -import { join, sep } from "node:path"; -import { globSync } from "node:fs"; +import { join } from "node:path"; + +import { walkSourceFiles } from "@/__tests__/helpers/source-files"; import type { SyncVerdict } from "@/lib/integrations/sync-verdict"; import { @@ -113,14 +114,10 @@ describe("structural guard — no second pill dialect", () => { const SRC = join(process.cwd(), "src"); function sourceFiles(): string[] { - return globSync("**/*.{ts,tsx}", { cwd: SRC }) - .filter( - (p) => !p.startsWith(`generated${sep}`) && !p.startsWith("generated/"), - ) + return walkSourceFiles(SRC, { floor: 3000 }) + .filter((p) => !p.startsWith("generated/")) .filter((p) => !p.includes("__tests__")) - .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")) - .map((p) => p.split(sep).join("/")) - .sort(); + .filter((p) => !p.endsWith(".test.ts") && !p.endsWith(".test.tsx")); } /** diff --git a/src/lib/__tests__/i18n-locale-integrity.test.ts b/src/lib/__tests__/i18n-locale-integrity.test.ts index 0fb001b28..267a21b32 100644 --- a/src/lib/__tests__/i18n-locale-integrity.test.ts +++ b/src/lib/__tests__/i18n-locale-integrity.test.ts @@ -2,6 +2,8 @@ import { describe, it, expect } from "vitest"; import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; +import { requireFloor } from "@/__tests__/helpers/source-files"; + const ROOT = join(__dirname, "../../.."); const MESSAGES_DIR = join(ROOT, "messages"); const EN_PATH = join(MESSAGES_DIR, "en.json"); @@ -20,7 +22,9 @@ function discoverLocales(): Array<{ locale: string; path: string }> { })); } -const ALL_LOCALES = discoverLocales(); +// Every parity check below iterates this list, so an empty discovery would +// run none of them and pass. Seven bundles ship today. +const ALL_LOCALES = requireFloor("message bundles", discoverLocales(), 7); const NON_EN_LOCALES = ALL_LOCALES.filter((l) => l.locale !== "en"); // JSON.parse silently keeps the last value when an object has duplicate keys, diff --git a/src/lib/ai/coach/__tests__/coach-locale-no-german-default.test.ts b/src/lib/ai/coach/__tests__/coach-locale-no-german-default.test.ts index c295304c1..68ab22cca 100644 --- a/src/lib/ai/coach/__tests__/coach-locale-no-german-default.test.ts +++ b/src/lib/ai/coach/__tests__/coach-locale-no-german-default.test.ts @@ -16,6 +16,8 @@ import { describe, it, expect } from "vitest"; import { readdirSync, readFileSync } from "node:fs"; import { resolve } from "node:path"; +import { requireFloor } from "@/__tests__/helpers/source-files"; + const ROOT = resolve(__dirname, "../../../.."); /** @@ -23,10 +25,14 @@ const ROOT = resolve(__dirname, "../../../.."); * so every module there is guarded — read from the directory, so a step added * later is covered without editing this list. */ -const TURN_PIPELINE_FILES = readdirSync(resolve(ROOT, "lib/ai/coach/turn")) - .filter((name) => name.endsWith(".ts")) - .sort() - .map((name) => `lib/ai/coach/turn/${name}`); +const TURN_PIPELINE_FILES = requireFloor( + "coach turn pipeline modules", + readdirSync(resolve(ROOT, "lib/ai/coach/turn")) + .filter((name) => name.endsWith(".ts")) + .sort() + .map((name) => `lib/ai/coach/turn/${name}`), + 12, +); const GUARDED_FILES = [ "app/api/insights/chat/route.ts", diff --git a/src/lib/insights/eval/__tests__/tone-harness.test.ts b/src/lib/insights/eval/__tests__/tone-harness.test.ts index a6e9b2e99..c5ab6f687 100644 --- a/src/lib/insights/eval/__tests__/tone-harness.test.ts +++ b/src/lib/insights/eval/__tests__/tone-harness.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { requireFloor } from "@/__tests__/helpers/source-files"; import { locales, type Locale } from "@/lib/i18n/config"; import { openingShape } from "@/lib/ai/prompts/shared-contracts"; import { @@ -110,14 +111,18 @@ describe("tone harness — prompt surfaces", () => { "native-prompts.ts", "status-batch.ts", ]); - const modules = readdirSync(dir).filter( - (f) => - f.endsWith(".ts") && - !f.endsWith(".d.ts") && - !NON_ASSESSMENT.has(f) && - /export function get\w*SystemPrompt/.test( - readFileSync(join(dir, f), "utf8"), - ), + const modules = requireFloor( + "assessment prompt modules", + readdirSync(dir).filter( + (f) => + f.endsWith(".ts") && + !f.endsWith(".d.ts") && + !NON_ASSESSMENT.has(f) && + /export function get\w*SystemPrompt/.test( + readFileSync(join(dir, f), "utf8"), + ), + ), + 11, ); const registered = new Set( ASSESSMENT_SURFACES.map((s) => diff --git a/src/lib/jobs/__tests__/job-outcome-source-inventory.test.ts b/src/lib/jobs/__tests__/job-outcome-source-inventory.test.ts index 02197386b..dc399f983 100644 --- a/src/lib/jobs/__tests__/job-outcome-source-inventory.test.ts +++ b/src/lib/jobs/__tests__/job-outcome-source-inventory.test.ts @@ -13,6 +13,7 @@ import { join } from "node:path"; import ts from "typescript"; import { describe, expect, it } from "vitest"; +import { requireFloor } from "@/__tests__/helpers/source-files"; import { JOB_FACT_ALLOWLIST } from "@/lib/jobs/job-outcome"; const JOBS_ROOT = join(process.cwd(), "src/lib/jobs"); @@ -109,14 +110,18 @@ function successFactKeys(file: string): string[] { describe("durable production job-outcome fact inventory", () => { it("allows every explicit production success-fact key", () => { - const unknown = productionJobFiles() - .flatMap((file) => - successFactKeys(file).map((key) => ({ - file: file.replace(`${process.cwd()}/`, ""), - key, - })), - ) - .filter(({ key }) => !JOB_FACT_ALLOWLIST.has(key)); + const facts = requireFloor( + "jobDone fact keys", + requireFloor("production job files", productionJobFiles(), 120).flatMap( + (file) => + successFactKeys(file).map((key) => ({ + file: file.replace(`${process.cwd()}/`, ""), + key, + })), + ), + 350, + ); + const unknown = facts.filter(({ key }) => !JOB_FACT_ALLOWLIST.has(key)); expect(unknown).toEqual([]); }); diff --git a/src/lib/medications/side-effects/__tests__/drift-guard.test.ts b/src/lib/medications/side-effects/__tests__/drift-guard.test.ts index 56b363d37..0037bda66 100644 --- a/src/lib/medications/side-effects/__tests__/drift-guard.test.ts +++ b/src/lib/medications/side-effects/__tests__/drift-guard.test.ts @@ -44,6 +44,13 @@ function sortedKeys(keys: readonly T[]): T[] { } describe("side-effect taxonomy drift guard", () => { + it("compares populated keysets, not two empty ones", () => { + // Every check below is an equality, and two empty sets are equal. A + // mocked or mis-resolved enum would pass all of them on nothing. + expect(Object.values(MedicationSideEffectCategory).length).toBe(5); + expect(Object.values(MedicationSideEffectEntry).length).toBe(21); + }); + describe("category keysets", () => { it("validator categories equal Prisma enum categories", () => { const prisma = Object.values(MedicationSideEffectCategory);