Files
HealthLog/src/__tests__/guard-dynamic-import-guard.test.ts
T
Marc-André Bombeck 3e8846929c test(guards): load no module graph inside a guard's test body
step-up-elevation-guard timed out under load in three separate gates
and passed every time on its own. One of its tests imported
@/lib/auth/step-up inside the test body to read the fresh-factor set,
and that module reaches the database client and the generated Prisma
client: the import alone ran against the test timeout, and how long it
took depended on the machine, not on the code under test.

The fresh-factor definitions (StepUpMethod, FRESH_FACTOR_METHODS,
isFreshFactorMethod) move to step-up-factors.ts, which imports nothing;
the guard imports it statically and the test is synchronous. Two more
guards did the same: next-image-optimizer-guard loaded next.config and
its plugins per test, and proxy-relative-redirect-guard loaded the
redirect helper with next/server. Both import statically now.

guard-dynamic-import-guard.test.ts parses every file of the guard
project with the TypeScript parser and fails on any import() call, so
a guard that needs a value imports it with the file or reads the
source; a test that must load late belongs to the isolated project.
2026-10-23 22:51:51 +02:00

97 lines
3.2 KiB
TypeScript

/**
* No structural guard loads a module graph inside a test.
*
* The guard project shares one module cache per worker (`isolate: false`,
* `tests/unit-projects.ts`), so a guard's static imports load once, at
* collection. A dynamic `import()` inside a test body loads its graph against
* that test's timeout instead, and how long it takes depends on the machine
* rather than on the code under test. `step-up-elevation-guard` imported
* `@/lib/auth/step-up` that way, which reached the database client and the
* generated Prisma client, and timed out under load in three separate gates
* while passing every time on its own. `next-image-optimizer-guard` loaded
* Next's config and plugins the same way.
*
* So no file of the guard project calls `import()`. A guard that needs a
* value imports it statically, or reads the source; a test that must load a
* module late is an isolated test, not a guard. The files are read with the
* TypeScript parser, so an `import(` inside a string (a matcher's own test
* case) is not one.
*/
import { join } from "node:path";
import ts from "typescript";
import { describe, expect, it } from "vitest";
import { classifyUnitFiles } from "../../tests/unit-projects";
import {
readSourceText,
requireFloor,
walkSourceFiles,
} from "./helpers/source-files";
const ROOT = join(__dirname, "..", "..");
/** The guard project's files, classified the way the config classifies them. */
function guardFiles(): string[] {
const under = (dir: string, floor: number, extensions?: string[]) =>
walkSourceFiles(join(ROOT, dir), { floor, extensions }).map(
(rel) => `${dir}/${rel}`,
);
const candidates = [
...under("src/__tests__", 200),
...under("scripts/__tests__", 5),
...under("tests/quarantine", 1),
...under("eslint-plugins", 10, [".mjs"]),
].filter((rel) => /\.test\.(?:tsx?|mjs)$/.test(rel));
return classifyUnitFiles(ROOT, candidates).guards;
}
function dynamicImports(rel: string): number[] {
const text = readSourceText(join(ROOT, rel));
const file = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true);
const lines: number[] = [];
const visit = (node: ts.Node) => {
if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword
) {
lines.push(file.getLineAndCharacterOfPosition(node.getStart()).line + 1);
}
ts.forEachChild(node, visit);
};
visit(file);
return lines;
}
describe("guards load their modules with the file", () => {
const files = requireFloor("guard files", guardFiles(), 150);
it("finds a dynamic import where there is one", () => {
const probe = ts.createSourceFile(
"probe.ts",
'it("x", async () => { await import("@/lib/db"); }); const s = "import(\\"a\\")";',
ts.ScriptTarget.Latest,
true,
);
let found = 0;
const visit = (node: ts.Node) => {
if (
ts.isCallExpression(node) &&
node.expression.kind === ts.SyntaxKind.ImportKeyword
) {
found += 1;
}
ts.forEachChild(node, visit);
};
visit(probe);
expect(found).toBe(1);
});
it("calls import() in no guard", () => {
const found = files.flatMap((rel) =>
dynamicImports(rel).map((line) => `${rel}:${line}`),
);
expect(found).toEqual([]);
});
});