mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
test: fail any structural guard that cannot fail on an empty sweep
guard-floor-guard reads every *-guard(s).test file under src/ and every test that walks a directory, parses it with the TypeScript compiler, and fails unless it asserts a floor on what it found: requireFloor, scanSourceMatches, toBeGreaterThan, an exact non-zero count, or an exact non-empty set. A floor on the walk alone does not count; the Bearer-scope guard had one and still matched nothing for weeks. Files that are not scanners (unit tests of runtime functions named "guard", fixed file lists with a positive match per file, temp-directory listings) are named with a reason. An exemption that goes stale fails the run. The reader's own shapes are pinned by probe sources, including the ones it must not count: comments, strings, .not, zero floors, it.skip. It answers per file, not per matcher, and says so.
This commit is contained in:
1 parent
b4577f5c31
commit
f3bf65fb4e
2 files changed
+443
No files matched your search
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Every structural guard asserts that it found something.
|
||||
*
|
||||
* A guard that sweeps the tree and compares the result against an allowlist,
|
||||
* or against "none of these may exist", passes on an empty sweep. That is not
|
||||
* hypothetical here: the Bearer-scope guard stayed green for weeks because its
|
||||
* matcher demanded `apiToken.findUnique(` on one line and the one file that
|
||||
* mattered wrote it across two. The sweep matched nothing, nothing disagreed
|
||||
* with the allowlist, and the guard reported a clean tree.
|
||||
*
|
||||
* So this reads every guard and fails unless it states a floor — a count it
|
||||
* must reach — or is named below with the reason it does not need one.
|
||||
*
|
||||
* ## Scope
|
||||
*
|
||||
* Every test file under `src/` named `*-guard.test.ts(x)` or
|
||||
* `*-guards.test.ts(x)`, plus every test file that walks a directory
|
||||
* (`readdirSync`, `globSync`, `walkSourceFiles`), whatever it is called: the
|
||||
* inventories and coverage checks are the same machine under another name.
|
||||
*
|
||||
* ## What counts as a floor
|
||||
*
|
||||
* The shapes `analyseGuardSource` recognises: `requireFloor`,
|
||||
* `scanSourceMatches`, `toBeGreaterThan(n ≥ 0)`, `toBeGreaterThanOrEqual(n ≥
|
||||
* 1)`, `.not.toHaveLength(0)`, `toHaveLength(n ≥ 1)`, an exact
|
||||
* `expect(x.length).toBe(n ≥ 1)`, and an exact `toEqual([...])` against a
|
||||
* non-empty literal set. A floor on the walk alone (`walkSourceFiles`'s own
|
||||
* `floor`) does not count: it proves the tree was read, not that the matcher
|
||||
* found anything in it, and the Bearer guard had one.
|
||||
*
|
||||
* ## Its limit
|
||||
*
|
||||
* It answers per file. A guard with three matchers and a floor under one of
|
||||
* them passes. Reviewers still own the question of which set the floor sits
|
||||
* under.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { analyseGuardSource } from "./helpers/guard-floor";
|
||||
import {
|
||||
requireFloor,
|
||||
scanSourceMatches,
|
||||
walkSourceFiles,
|
||||
} from "./helpers/source-files";
|
||||
|
||||
const SRC = join(process.cwd(), "src");
|
||||
|
||||
/**
|
||||
* In scope, without a floor, and why that is right. Paths relative to `src/`.
|
||||
* An entry that gains a floor, or whose file disappears, fails the run, so the
|
||||
* list cannot outlive its reasons.
|
||||
*/
|
||||
const NO_FLOOR_NEEDED: Readonly<Record<string, string>> = {
|
||||
// Fixed file lists. Each file is read by name, so a missing one throws,
|
||||
// and each must positively match the shape it is supposed to keep.
|
||||
"__tests__/backup-readers-stream-guard.test.ts":
|
||||
"named readers, each must positively reference the streaming reader",
|
||||
"__tests__/proxy-relative-redirect-guard.test.ts":
|
||||
"named routes, each must positively call relativeRedirect(",
|
||||
"__tests__/rotation-walk-keyset-guard.test.ts":
|
||||
"named walks, each must positively match the keyset cursor",
|
||||
"components/charts/__tests__/touch-action-guard.test.ts":
|
||||
"named chart wrappers, each must positively declare touch-pan-y",
|
||||
"__tests__/moodlog-removal-guard.test.ts":
|
||||
"absence checks over named paths, columns and keys; no matcher to drift",
|
||||
|
||||
// Unit tests of runtime code whose name ends in "guard". They read no
|
||||
// source and sweep nothing.
|
||||
"app/__tests__/error-chunk-reload-guard.test.ts":
|
||||
"unit test of the chunk-reload guard function",
|
||||
"app/api/insights/chat/__tests__/route-fence-guard.test.ts":
|
||||
"route behaviour test of the chat fence",
|
||||
"app/api/insights/chat/__tests__/route-guard.test.ts":
|
||||
"route behaviour test of the outbound screen",
|
||||
"lib/ai/__tests__/consent-guard.test.ts": "unit test of the consent gate",
|
||||
"lib/ai/coach/__tests__/outbound-guard.test.ts":
|
||||
"unit test of the outbound screen",
|
||||
"lib/analytics/score/__tests__/config-delta-guard.test.ts":
|
||||
"unit test of the score delta boundary",
|
||||
"lib/medications/__tests__/route-guards.test.ts":
|
||||
"unit test of the ownership helper",
|
||||
|
||||
// List a temporary directory the test itself filled.
|
||||
"lib/import/__tests__/apple-health-staging.test.ts":
|
||||
"lists its own temp staging directory",
|
||||
"lib/multipart/__tests__/stream-to-disk.test.ts":
|
||||
"lists its own temp directory",
|
||||
};
|
||||
|
||||
const NAMED_GUARD = /-guards?\.test\.tsx?$/;
|
||||
|
||||
interface Scoped {
|
||||
rel: string;
|
||||
named: boolean;
|
||||
floors: string[];
|
||||
}
|
||||
|
||||
function guardsInScope(): Scoped[] {
|
||||
const scoped: Scoped[] = [];
|
||||
const tests = walkSourceFiles(SRC, { floor: 3000 })
|
||||
.filter((rel) => !rel.startsWith("generated/"))
|
||||
.filter((rel) => /\.test\.tsx?$/.test(rel));
|
||||
for (const rel of requireFloor("test files under src/", tests, 1500)) {
|
||||
const named = NAMED_GUARD.test(rel);
|
||||
const report = analyseGuardSource(
|
||||
rel,
|
||||
readFileSync(join(SRC, rel), "utf8"),
|
||||
);
|
||||
if (!named && !report.walksDirectories) continue;
|
||||
scoped.push({ rel, named, floors: report.floors });
|
||||
}
|
||||
return scoped;
|
||||
}
|
||||
|
||||
/** Floors that say something about matches, not just about the walk. */
|
||||
function matchFloors(floors: readonly string[]): string[] {
|
||||
return floors.filter((f) => f !== "walkSourceFiles");
|
||||
}
|
||||
|
||||
describe("every structural guard states a floor", () => {
|
||||
const scoped = guardsInScope();
|
||||
|
||||
it("finds the guards (this check has a floor too)", () => {
|
||||
expect(scoped.filter((g) => g.named).length).toBeGreaterThanOrEqual(100);
|
||||
expect(scoped.filter((g) => !g.named).length).toBeGreaterThanOrEqual(40);
|
||||
});
|
||||
|
||||
it("each one asserts a floor on what it found, or says why not", () => {
|
||||
const missing = scoped
|
||||
.filter((g) => matchFloors(g.floors).length === 0)
|
||||
.filter((g) => !(g.rel in NO_FLOOR_NEEDED))
|
||||
.map((g) => g.rel);
|
||||
expect(
|
||||
missing,
|
||||
"These guards can pass on a sweep that found nothing. Add a floor " +
|
||||
"(requireFloor, scanSourceMatches, or expect(count).toBeGreaterThan…) " +
|
||||
"pinned at the real count, or name the file in NO_FLOOR_NEEDED with " +
|
||||
"the reason it is not a scanner.",
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("names no file in the exemption list that no longer needs it", () => {
|
||||
const byRel = new Map(scoped.map((g) => [g.rel, g]));
|
||||
const stale = Object.keys(NO_FLOOR_NEEDED).filter((rel) => {
|
||||
const g = byRel.get(rel);
|
||||
return !g || matchFloors(g.floors).length > 0;
|
||||
});
|
||||
expect(
|
||||
stale,
|
||||
"Exempt files that are gone, out of scope, or now carry a floor",
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the floor reader counts what it should and nothing else", () => {
|
||||
const floorsOf = (source: string) =>
|
||||
matchFloors(analyseGuardSource("probe.test.ts", source).floors);
|
||||
|
||||
it.each([
|
||||
["requireFloor", `requireFloor("x", found, 3);`],
|
||||
["toBeGreaterThan(0)", `expect(found.length).toBeGreaterThan(0);`],
|
||||
["toBeGreaterThanOrEqual(n)", `expect(n).toBeGreaterThanOrEqual(12);`],
|
||||
["a named floor", `expect(n).toBeGreaterThanOrEqual(FLOOR);`],
|
||||
["not.toHaveLength(0)", `expect(found).not.toHaveLength(0);`],
|
||||
["exact length", `expect(found.length).toBe(7);`],
|
||||
["exact non-empty set", `expect(found).toEqual(["lib/a.ts"]);`],
|
||||
[
|
||||
"scanSourceMatches",
|
||||
`scanSourceMatches(root, /x/g, { fileFloor: 10, matchFloor: 2 });`,
|
||||
],
|
||||
])("counts %s", (_label, source) => {
|
||||
expect(floorsOf(source)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a floor in a comment", `// expect(n).toBeGreaterThan(0);`],
|
||||
["a floor in a string", `const s = "expect(n).toBeGreaterThan(0)";`],
|
||||
["a negated floor", `expect(n).not.toBeGreaterThan(0);`],
|
||||
["GreaterThanOrEqual(0)", `expect(n).toBeGreaterThanOrEqual(0);`],
|
||||
["a zero requireFloor", `requireFloor("x", found, 0);`],
|
||||
["a zero matchFloor", `scanSourceMatches(r, /x/g, { matchFloor: 0 });`],
|
||||
["an empty-set equality", `expect(offenders).toEqual([]);`],
|
||||
["a spread-only set", `expect(found).toEqual([...EXPECTED]);`],
|
||||
["toHaveLength(0)", `expect(found).toHaveLength(0);`],
|
||||
["a walk floor alone", `walkSourceFiles(SRC, { floor: 3000 });`],
|
||||
[
|
||||
"a floor inside it.skip",
|
||||
`it.skip("x", () => { expect(n).toBeGreaterThan(0); });`,
|
||||
],
|
||||
])("does not count %s", (_label, source) => {
|
||||
expect(floorsOf(source)).toEqual([]);
|
||||
});
|
||||
|
||||
it("still notices the walk, so a walker stays in scope", () => {
|
||||
const report = analyseGuardSource(
|
||||
"probe.test.ts",
|
||||
`const files = readdirSync(dir);`,
|
||||
);
|
||||
expect(report.walksDirectories).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the floor helpers refuse an empty answer", () => {
|
||||
it("requireFloor throws below the floor and refuses a zero floor", () => {
|
||||
expect(() => requireFloor("probe", [], 1)).toThrow(/below the stated/);
|
||||
expect(() => requireFloor("probe", [1, 2], 0)).toThrow(/positive/);
|
||||
expect(requireFloor("probe", [1, 2], 2)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("scanSourceMatches floors files and matches separately", () => {
|
||||
const helpers = join(SRC, "__tests__", "helpers");
|
||||
const hits = scanSourceMatches(helpers, /export function \w+/g, {
|
||||
fileFloor: 5,
|
||||
matchFloor: 5,
|
||||
});
|
||||
expect(hits.some((h) => h.file === "source-files.ts")).toBe(true);
|
||||
expect(() =>
|
||||
scanSourceMatches(helpers, /no such text anywhere\u0000/g, {
|
||||
fileFloor: 5,
|
||||
matchFloor: 1,
|
||||
}),
|
||||
).toThrow(/below the stated floor/);
|
||||
expect(() =>
|
||||
scanSourceMatches(helpers, /export/, { fileFloor: 5, matchFloor: 1 }),
|
||||
).toThrow(/g flag/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Reads a structural guard's source and answers one question: does it assert
|
||||
* that it found something?
|
||||
*
|
||||
* A guard that sweeps the tree and compares what it found against an
|
||||
* allowlist, or against "none of these may exist", passes on an empty sweep.
|
||||
* So every scanner has to say, somewhere, that its sweep was not empty. The
|
||||
* shapes that count as saying it are listed on `FLOOR_SHAPES` below; anything
|
||||
* else is not a floor.
|
||||
*
|
||||
* This parses the file with the TypeScript compiler rather than grepping it,
|
||||
* so a floor that only appears in a comment, inside a string, under `.not`, or
|
||||
* inside an `it.skip` does not count.
|
||||
*
|
||||
* Its honest limit: it answers per file, not per matcher. A guard with three
|
||||
* matchers and a floor on one of them passes here. The floor proves the file
|
||||
* read something; it does not prove every rule in the file did.
|
||||
*/
|
||||
import ts from "typescript";
|
||||
|
||||
export type FloorShape =
|
||||
| "walkSourceFiles"
|
||||
| "scanSourceMatches"
|
||||
| "requireFloor"
|
||||
| "toBeGreaterThan"
|
||||
| "toBeGreaterThanOrEqual"
|
||||
| "not.toHaveLength(0)"
|
||||
| "toHaveLength(n>0)"
|
||||
| "length.toBe(n>0)"
|
||||
| "toEqual([non-empty])";
|
||||
|
||||
/** Calls that make a file a directory scanner, and so put it in scope. */
|
||||
const WALKERS = new Set(["readdirSync", "globSync", "walkSourceFiles"]);
|
||||
|
||||
function numericValue(node: ts.Expression | undefined): number | null {
|
||||
if (!node) return null;
|
||||
if (ts.isNumericLiteral(node)) return Number(node.text);
|
||||
if (
|
||||
ts.isPrefixUnaryExpression(node) &&
|
||||
node.operator === ts.SyntaxKind.MinusToken &&
|
||||
ts.isNumericLiteral(node.operand)
|
||||
) {
|
||||
return -Number(node.operand.text);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function calleeName(call: ts.CallExpression): string | null {
|
||||
const callee = call.expression;
|
||||
if (ts.isIdentifier(callee)) return callee.text;
|
||||
if (ts.isPropertyAccessExpression(callee)) return callee.name.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `x.not.toBeGreaterThan(…)` — the matcher sits under a `.not`. */
|
||||
function isNegated(call: ts.CallExpression): boolean {
|
||||
const callee = call.expression;
|
||||
return (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isPropertyAccessExpression(callee.expression) &&
|
||||
callee.expression.name.text === "not"
|
||||
);
|
||||
}
|
||||
|
||||
/** `expect(<anything>.length, …).toBe(…)` — the subject is a length. */
|
||||
function expectSubjectIsLength(call: ts.CallExpression): boolean {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return false;
|
||||
const expectCall = callee.expression;
|
||||
if (
|
||||
!ts.isCallExpression(expectCall) ||
|
||||
!ts.isIdentifier(expectCall.expression) ||
|
||||
expectCall.expression.text !== "expect"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const subject = expectCall.arguments[0];
|
||||
return (
|
||||
subject !== undefined &&
|
||||
ts.isPropertyAccessExpression(subject) &&
|
||||
subject.name.text === "length"
|
||||
);
|
||||
}
|
||||
|
||||
/** Inside `it.skip(…)`, `describe.todo(…)`, `test.skip(…)` and friends. */
|
||||
function isInSkippedBlock(node: ts.Node): boolean {
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (
|
||||
ts.isCallExpression(cur) &&
|
||||
ts.isPropertyAccessExpression(cur.expression) &&
|
||||
["skip", "todo"].includes(cur.expression.name.text) &&
|
||||
ts.isIdentifier(cur.expression.expression) &&
|
||||
["it", "test", "describe"].includes(cur.expression.expression.text)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The value of property `key` on an object-literal argument, if any. */
|
||||
function objectProperty(
|
||||
arg: ts.Expression | undefined,
|
||||
key: string,
|
||||
): ts.Expression | "shorthand" | undefined {
|
||||
if (!arg || !ts.isObjectLiteralExpression(arg)) return undefined;
|
||||
for (const prop of arg.properties) {
|
||||
if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === key) {
|
||||
return "shorthand";
|
||||
}
|
||||
if (
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
|
||||
prop.name.text === key
|
||||
) {
|
||||
return prop.initializer;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** A floor value counts unless it is a literal below `min`. */
|
||||
function floorValueCounts(
|
||||
value: ts.Expression | "shorthand" | undefined,
|
||||
min: number,
|
||||
): boolean {
|
||||
if (value === undefined) return false;
|
||||
if (value === "shorthand") return true;
|
||||
const n = numericValue(value);
|
||||
return n === null || n >= min;
|
||||
}
|
||||
|
||||
function floorShapeOf(call: ts.CallExpression): FloorShape | null {
|
||||
const name = calleeName(call);
|
||||
const [first, second, third] = call.arguments;
|
||||
switch (name) {
|
||||
case "walkSourceFiles":
|
||||
return floorValueCounts(objectProperty(second, "floor"), 1)
|
||||
? "walkSourceFiles"
|
||||
: null;
|
||||
case "scanSourceMatches":
|
||||
return floorValueCounts(objectProperty(third, "matchFloor"), 1)
|
||||
? "scanSourceMatches"
|
||||
: null;
|
||||
case "requireFloor":
|
||||
return floorValueCounts(third, 1) ? "requireFloor" : null;
|
||||
case "toBeGreaterThan":
|
||||
if (isNegated(call)) return null;
|
||||
return floorValueCounts(first, 0) ? "toBeGreaterThan" : null;
|
||||
case "toBeGreaterThanOrEqual":
|
||||
if (isNegated(call)) return null;
|
||||
return floorValueCounts(first, 1) ? "toBeGreaterThanOrEqual" : null;
|
||||
case "toHaveLength": {
|
||||
const n = numericValue(first);
|
||||
if (isNegated(call)) return n === 0 ? "not.toHaveLength(0)" : null;
|
||||
return n !== null && n >= 1 ? "toHaveLength(n>0)" : null;
|
||||
}
|
||||
case "toBe":
|
||||
case "toEqual":
|
||||
case "toStrictEqual": {
|
||||
if (isNegated(call)) return null;
|
||||
// `expect(found).toEqual(["lib/one.ts"])` — the exact, non-empty set.
|
||||
if (
|
||||
name !== "toBe" &&
|
||||
first !== undefined &&
|
||||
ts.isArrayLiteralExpression(first) &&
|
||||
first.elements.some((el) => !ts.isSpreadElement(el))
|
||||
) {
|
||||
return "toEqual([non-empty])";
|
||||
}
|
||||
// `expect(found.length).toBe(12)` — an exact non-zero count.
|
||||
const n = numericValue(first);
|
||||
if (n === null || n < 1) return null;
|
||||
return expectSubjectIsLength(call) ? "length.toBe(n>0)" : null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GuardFloorReport {
|
||||
/** Every floor-asserting call reachable outside a skipped block. */
|
||||
floors: FloorShape[];
|
||||
/** True when the file calls a directory walker. */
|
||||
walksDirectories: boolean;
|
||||
}
|
||||
|
||||
export function analyseGuardSource(
|
||||
fileName: string,
|
||||
source: string,
|
||||
): GuardFloorReport {
|
||||
const sf = ts.createSourceFile(
|
||||
fileName,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const floors: FloorShape[] = [];
|
||||
let walksDirectories = false;
|
||||
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const name = calleeName(node);
|
||||
if (name && WALKERS.has(name)) walksDirectories = true;
|
||||
const shape = floorShapeOf(node);
|
||||
if (shape && !isInSkippedBlock(node)) floors.push(shape);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sf);
|
||||
|
||||
return { floors, walksDirectories };
|
||||
}
|
||||
Reference in new issue
Block a user