mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test: give every scanning guard a floor on what it found
A guard that compares a sweep against an allowlist, or against "none of
these may exist", passes when the sweep comes back empty. walkSourceFiles
already refuses a short walk; this adds the same refusal for what a guard
does with the walk.
- requireFloor(label, found, floor) throws below the floor and refuses a
floor of zero.
- scanSourceMatches(root, pattern, { fileFloor, matchFloor }) walks, matches
and floors both the files read and the hits found.
Floors added, pinned at the current counts:
- coach_usage owner columns: the clauses it actually examined (6).
- measurement reference index: the foreign keys it extracted (2).
- job outcome fact inventory: job files (130, floor 120) and fact keys
(399, floor 350).
- tone harness: assessment prompt modules (11).
- coach locale default: turn pipeline modules (15, floor 12).
- locale integrity: message bundles (7).
- settings card header coverage and settings anatomy: the walked cards.
- coach metric scope inventory: insight route sources.
- side-effect drift guard: both enums populated, since two empty keysets
are equal.
The pill-mapper guard walked with globSync, which skips dot-prefixed
directories; it now walks through walkSourceFiles like the rest.
This commit is contained in:
1 parent
a3b5480d04
commit
b4577f5c31
12 files changed
+166
-37
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -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<T>(
|
||||
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[];
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string> {
|
||||
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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <h2 text-lg> 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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
@@ -44,6 +44,13 @@ function sortedKeys<T extends string>(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);
|
||||
|
||||
Reference in new issue
Block a user