Files
siftlode/frontend/src/lib/gates.contract.test.ts
T
peter 182ec31120 test(gates): hold the lint and lane configs to a committed snapshot
The register test detected narrowings by modelling config shapes, and each
review round found one more shape it did not model (a basePath global ignore,
a rule src never declares, a widened glob filed under a narrow row, a substring
check on a CI line, knip.json beside knip.jsonc). It now compares instead:
gates.snapshot.json holds ESLint's effective config for every code file and the
lines that run each lane, and any difference fails until it is regenerated.
The register is read off the snapshot's configs, which surfaced the ui/ and
lib/ test scopes the sampling never reached.

The complexity ratchet also names a wrapped function by its binding
(const onSave = useCallback(() => ...)), not as "useCallback callback".
2026-10-04 04:47:10 +02:00

591 lines
26 KiB
TypeScript

import { createHash } from "node:crypto";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { relative } from "node:path";
import { fileURLToPath } from "node:url";
import { ESLint, type Linter } from "eslint";
import prettierConfig from "eslint-config-prettier";
import { format, resolveConfig } from "prettier";
import { parse as parseToml } from "smol-toml";
import ts from "typescript";
import tseslint from "typescript-eslint";
import { describe, expect, it } from "vitest";
// The gate register (docs/gates.md) is held to the EFFECTIVE configuration (E32). A narrowing that
// is not on the register fails here — a rule switched off for a scope, a weak compiler flag, a ruff
// ignore, a knip exclusion, an accepted CVE. So "the gate checks X" is a claim this test verifies,
// not one a comment makes. (The comment that claimed the React Compiler's rules "are also linted"
// stood for months while the rule that would have said so was off.)
//
// Each narrowing is a (scope, id) pair; the register must hold a table row whose first cell is the
// scope and which names the id in backticks.
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
const FRONTEND = `${REPO}frontend/`;
const read = (path: string) => readFileSync(REPO + path, "utf8");
/** register scope → every backticked id on its rows. */
function register(): Map<string, Set<string>> {
const rows = new Map<string, Set<string>>();
for (const line of read("docs/gates.md").split("\n")) {
if (!line.startsWith("|") || /^\|(\s*-+\s*\|)+\s*$/.test(line)) continue; // not the |---| rule
// a row may cover several scopes the same way: `src tests · ui tests · lib tests`
for (const scope of line
.split("|")[1]!
.split(" · ")
.map((s) => s.trim())) {
const ids = rows.get(scope) ?? new Set<string>();
for (const m of line.matchAll(/`([^`]+)`/g)) ids.add(m[1]!);
rows.set(scope, ids);
}
}
return rows;
}
function missingFrom(found: [scope: string, id: string][]): string[] {
const reg = register();
return found.filter(([scope, id]) => !reg.get(scope)?.has(id)).map(([s, id]) => `${s}: ${id}`);
}
const jsonc = (path: string) => {
const { config, error } = ts.parseConfigFileTextToJson(path, read(path));
if (error)
throw new Error(`${path}: ${ts.flattenDiagnosticMessageText(error.messageText, "\n")}`);
return config as Record<string, unknown>;
};
// --- ESLint ---------------------------------------------------------------------------------------
const severity = (v: Linter.RuleEntry | undefined): number => {
if (v === undefined) return -1; // not applied at all
const s = Array.isArray(v) ? v[0] : v;
return s === 2 || s === "error" ? 2 : s === 1 || s === "warn" ? 1 : 0;
};
// Formatting is Prettier's (its own lane): the rules eslint-config-prettier switches off are not a
// narrowing of anything.
const PRETTIER_OFF = new Set(
Object.entries(prettierConfig.rules ?? {})
.filter(([, v]) => severity(v as Linter.RuleEntry) === 0)
.map(([r]) => r),
);
// …and the core rules typescript-eslint's compatibility layer switches off for TS files because the
// type checker reports them itself (no-undef, no-redeclare, no-dupe-keys, …).
const TSC_OWNED = new Set(
Object.entries(tseslint.configs.eslintRecommended.rules ?? {})
.filter(([, v]) => severity(v as Linter.RuleEntry) === 0)
.map(([r]) => r),
);
const family = (rule: string) => (rule.includes("/") ? `${rule.split("/")[0]}/*` : rule);
// A rule option whose key says it lets something through (`allowShortCircuit`,
// `allowForKnownSafeCalls`, `ignoreRestSiblings`, `exceptAfterSingleLine`) narrows the rule, and a
// `no-restricted-*` list re-declared for a scope narrows it by every entry it leaves out.
const RELAXING_KEY = /^(allow|ignore|except)/;
const relaxes = (options: unknown[]): boolean =>
options.some(
(o) =>
o !== null &&
typeof o === "object" &&
Object.entries(o).some(
([k, v]) => RELAXING_KEY.test(k) && v !== false && !(Array.isArray(v) && v.length === 0),
),
);
function restrictedEntries(entry: Linter.RuleEntry | undefined): Set<string> {
const opts: unknown[] = Array.isArray(entry) ? entry.slice(1) : [];
const flat = opts.flatMap((o) => {
if (o === null || typeof o !== "object" || Array.isArray(o)) return [o];
const { paths = [], patterns = [] } = o as { paths?: unknown[]; patterns?: unknown[] };
return "paths" in o || "patterns" in o ? [...paths, ...patterns] : [o];
});
return new Set(flat.map((e) => JSON.stringify(e)));
}
// --- the snapshot ---------------------------------------------------------------------------------
// The configs that can narrow a gate in more shapes than a check can enumerate are held to a
// COMMITTED SNAPSHOT (E32 review): ESLint's effective config for every file `eslint .` reaches, and
// the lines that run the lanes. Any change fails until the snapshot is regenerated, so a narrowing
// arrives as a diff of `gates.snapshot.json` in review instead of slipping past a detector. (Three
// review rounds kept finding shapes the detector this replaced did not model — a `basePath` global
// ignore, a widened glob filed under a narrow row, a substring check on a CI line. A comparison
// against everything has no shape to miss.)
//
// Regenerate: GATE_SNAPSHOT=update npx vitest run src/lib/gates.contract.test.ts
// then read the diff like any gate change. An ESLint config that changed keeps its scope name when
// most of its files had it; one no name fits is written as `new config N` — rename it to the
// register scope it is (the register test below then asks for its rows).
const SNAPSHOT = "frontend/src/lib/gates.snapshot.json";
const NOT_LINTED = "(not linted)";
type EslintConfig = {
rules: Record<string, unknown[]>;
linterOptions: Record<string, unknown>;
languageOptions: Record<string, unknown>;
plugins: string[];
settings: unknown;
};
type Snapshot = {
eslint: { configs: Record<string, EslintConfig>; files: Record<string, string[]> };
lanes: Record<string, unknown>;
};
const sortKeys = (v: unknown): unknown =>
Array.isArray(v)
? v.map(sortKeys)
: v !== null && typeof v === "object"
? Object.fromEntries(
Object.keys(v)
.sort()
.map((k) => [k, sortKeys((v as Record<string, unknown>)[k])]),
)
: v;
// through JSON, so a value compares as what the file can hold (a RegExp option is `{}` on both sides)
const canonical = <T>(v: T): T => JSON.parse(JSON.stringify(sortKeys(v) ?? null)) as T;
function normalize(cfg: Linter.Config): EslintConfig {
const lang = { ...(cfg.languageOptions ?? {}) } as Record<string, unknown>;
const parser = lang.parser as { meta?: { name?: string } } | undefined;
const parserOptions = { ...(lang.parserOptions as Record<string, unknown> | undefined) };
if (typeof parserOptions.tsconfigRootDir === "string")
// absolute: differs per machine
parserOptions.tsconfigRootDir = relative(FRONTEND, parserOptions.tsconfigRootDir) || ".";
// ~1200 browser globals per config: held by count and digest, not listed
const globals = Object.entries((lang.globals ?? {}) as Record<string, unknown>)
.map(([name, v]) => `${name}:${String(v)}`)
.sort();
const digest = createHash("sha1").update(globals.join("\n")).digest("hex").slice(0, 12);
return canonical({
languageOptions: {
...lang,
parser: parser ? (parser.meta?.name ?? "(unnamed parser)") : null,
parserOptions,
globals: `${globals.length} names, sha1 ${digest}`,
},
linterOptions: (cfg.linterOptions ?? {}) as Record<string, unknown>,
plugins: Object.keys(cfg.plugins ?? {}).sort(),
settings: cfg.settings ?? null,
rules: Object.fromEntries(
Object.entries(cfg.rules ?? {}).map(([rule, v]) => [
rule,
Array.isArray(v) ? [severity(v), ...v.slice(1)] : [severity(v)],
]),
),
});
}
// Every code file `eslint .` walks into. The skipped directories are the global ignores' generated
// output (the register's `everywhere` row) — untracked, and present on one machine but not another.
// Every other file is asked for its config, so a file a new ignore drops lands in `(not linted)`.
const WALK_SKIP = new Set([
"node_modules",
"dist",
"playwright-report",
"test-results",
"e2e/.auth",
]);
const CODE = /[.](?:[mc]?[jt]s|[jt]sx)$/;
function codeFiles(dir = ""): string[] {
return readdirSync(FRONTEND + dir, { withFileTypes: true }).flatMap((d) => {
const path = dir + d.name;
if (d.isDirectory()) return WALK_SKIP.has(path) ? [] : codeFiles(`${path}/`);
return CODE.test(d.name) ? [path] : [];
});
}
// On disk every config but src is written as its difference from src — the rules it sets otherwise
// and the src rules it does not apply — so a scope reads like its register rows, and one rule set
// is not repeated fourteen times. In memory it is always whole.
type Stored = Omit<EslintConfig, "rules"> & {
rules: Record<string, unknown[]>;
notApplied?: string[];
};
function toStored(configs: Record<string, EslintConfig>): Record<string, Stored> {
const src = configs.src?.rules;
if (!src) return configs;
return Object.fromEntries(
Object.entries(configs).map(([name, cfg]) => {
if (name === "src") return [name, cfg];
const rules = Object.fromEntries(
Object.entries(cfg.rules).filter(([r, v]) => JSON.stringify(v) !== JSON.stringify(src[r])),
);
return [
name,
{ ...cfg, rules, notApplied: Object.keys(src).filter((r) => !(r in cfg.rules)) },
];
}),
);
}
function fromStored(stored: Record<string, Stored>): Record<string, EslintConfig> {
const src = stored.src?.rules ?? {};
return Object.fromEntries(
Object.entries(stored).map(([name, { notApplied, ...cfg }]) => {
if (!notApplied) return [name, cfg];
const rules = { ...src, ...cfg.rules };
for (const r of notApplied) delete rules[r];
return [name, canonical({ ...cfg, rules })];
}),
);
}
const committed = (): Snapshot => {
const s = JSON.parse(read(SNAPSHOT)) as Snapshot & {
eslint: { configs: Record<string, Stored> };
};
return canonical({ ...s, eslint: { ...s.eslint, configs: fromStored(s.eslint.configs) } });
};
async function eslintActual(names: Snapshot["eslint"]): Promise<Snapshot["eslint"]> {
const eslint = new ESLint({ cwd: FRONTEND });
const groups = new Map<string, { config: EslintConfig; files: string[] }>();
const notLinted: string[] = [];
for (const file of codeFiles().sort()) {
const raw = (await eslint.calculateConfigForFile(file)) as Linter.Config | undefined;
if (!raw) {
notLinted.push(file);
continue;
}
const config = normalize(raw);
const key = JSON.stringify(config);
const group = groups.get(key) ?? { config, files: [] };
group.files.push(file);
groups.set(key, group);
}
// a config is named by the snapshot entry it equals; a changed one by the old name most of its
// files had (if still free), so the diff shows the rule that moved, not a renamed scope
const byContent = new Map(Object.entries(names.configs).map(([n, c]) => [JSON.stringify(c), n]));
const oldName = new Map(
Object.entries(names.files).flatMap(([n, files]) => files.map((f) => [f, n] as const)),
);
const configs: Record<string, EslintConfig> = {};
const files: Record<string, string[]> = { [NOT_LINTED]: notLinted };
const unnamed: { config: EslintConfig; files: string[] }[] = [];
for (const [key, group] of groups) {
const name = byContent.get(key);
if (name) [configs[name], files[name]] = [group.config, group.files];
else unnamed.push(group);
}
unnamed.forEach((group, i) => {
const votes = new Map<string, number>();
for (const f of group.files) {
const n = oldName.get(f);
if (n && n !== NOT_LINTED) votes.set(n, (votes.get(n) ?? 0) + 1);
}
const best = [...votes].sort((a, b) => b[1] - a[1])[0]?.[0];
const name = best && !(best in configs) ? best : `new config ${i + 1}`;
[configs[name], files[name]] = [group.config, group.files];
});
return canonical({ configs, files });
}
// the lines that run each lane, comments and blank lines aside
function meaningful(path: string): string[] {
return read(path)
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => {
const t = l.trim();
return t !== "" && !/^(#|\/\/|\/\*|\*)/.test(t);
});
}
const lanes = () => ({
".gitea/workflows/check.yml": meaningful(".gitea/workflows/check.yml"),
"backend/docker-compose.test.yml": meaningful("backend/docker-compose.test.yml"),
"backend/Dockerfile.test": meaningful("backend/Dockerfile.test"),
"frontend/package.json scripts": (
JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> }
).scripts,
"frontend/vitest.config.ts": meaningful("frontend/vitest.config.ts"),
"frontend/.prettierignore": meaningful("frontend/.prettierignore"),
});
let actual: Promise<Snapshot> | undefined;
const current = () =>
(actual ??= (async () =>
canonical({ eslint: await eslintActual(committed().eslint), lanes: lanes() }))());
// --- the register, read off the snapshot ------------------------------------------------------------
const ignorable = (rule: string, all: Record<string, unknown>) =>
PRETTIER_OFF.has(rule) ||
TSC_OWNED.has(rule) ||
// a core rule off because its typescript-eslint twin replaces it is reported as the twin
(!rule.includes("/") && `@typescript-eslint/${rule}` in all);
const entry = (v: unknown[] | undefined) => v as Linter.RuleEntry | undefined;
/** Every narrowing in the effective ESLint configs, as (scope, id). */
function eslintNarrowings({ configs, files }: Snapshot["eslint"]): [string, string][] {
const found: [string, string][] = files[NOT_LINTED]!.map((f) => [
"everywhere",
`${f} — not linted`,
]);
const src = configs.src?.rules;
if (!src) return [...found, ["src", "(no config named src)"]];
for (const [scope, { rules, linterOptions }] of Object.entries(configs)) {
// an unused `eslint-disable` is an error on every file, or a stale suppression stays armed
if (severity(linterOptions.reportUnusedDisableDirectives as Linter.RuleEntry) !== 2)
found.push([scope, "reportUnusedDisableDirectives"]);
const ids = new Set<string>();
for (const [rule, v] of Object.entries(rules)) {
if (ignorable(rule, rules)) continue;
const mine = entry(v)!;
const theirs = scope === "src" ? undefined : entry(src[rule]);
const relaxed = relaxes(v.slice(1)) && JSON.stringify(v) !== JSON.stringify(src[rule]);
if (theirs === undefined) {
// src, or a rule src does not have (the test and e2e plugins): at off or warn, or relaxed
if (severity(mine) < 2 || (scope === "src" ? relaxes(v.slice(1)) : relaxed)) ids.add(rule);
} else if (severity(mine) < severity(theirs)) ids.add(rule);
else if (rule.includes("no-restricted-")) {
const own = restrictedEntries(mine);
if ([...restrictedEntries(theirs)].some((e) => !own.has(e))) ids.add(rule);
} else if (relaxed) ids.add(rule);
}
// a src rule not applied at all here: one id for its whole family
if (scope !== "src")
for (const [rule, v] of Object.entries(src))
if (!(rule in rules) && !ignorable(rule, src) && severity(entry(v)) > 0)
ids.add(family(rule));
for (const id of ids) found.push([scope, id]);
}
return found;
}
describe("the gate snapshot (gates.snapshot.json)", () => {
it("holds the effective ESLint config of every file and the lines that run each lane", async () => {
const now = await current();
if (process.env.GATE_SNAPSHOT === "update") {
const path = REPO + SNAPSHOT;
const options = { ...(await resolveConfig(path)), filepath: path };
const stored = { ...now, eslint: { ...now.eslint, configs: toStored(now.eslint.configs) } };
writeFileSync(path, await format(JSON.stringify(sortKeys(stored)), options));
return;
}
expect(now).toEqual(committed());
}, 120_000);
});
describe("the gate register (docs/gates.md)", () => {
it("lists every ESLint rule narrowed, by scope", async () => {
expect(missingFrom(eslintNarrowings((await current()).eslint))).toEqual([]);
}, 120_000);
it("keeps --max-warnings 0, which is what makes a warn-level rule fail", () => {
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
expect(pkg.scripts.lint).toBe("eslint . --max-warnings 0");
});
// --- TypeScript ---------------------------------------------------------------------------------
const PROJECTS = [
"frontend/tsconfig.json",
"frontend/tsconfig.node.json",
"frontend/e2e/tsconfig.json",
];
const REQUIRED_ON = [
"strict",
"noUncheckedIndexedAccess",
"noUnusedLocals",
"noUnusedParameters",
"noFallthroughCasesInSwitch",
];
it.each(PROJECTS)("%s keeps the strict flags on", (path) => {
const opts = jsonc(path).compilerOptions as Record<string, unknown>;
expect(REQUIRED_ON.filter((flag) => opts[flag] !== true)).toEqual([]);
});
// `strict: true` is a bundle: a sub-flag set false beside it, an `extends` this test cannot see
// through, or a narrowed `include` / an `exclude` weakens the project while the five flags above
// still read true (E32 review).
const STRICT_FAMILY = [
"noImplicitAny",
"strictNullChecks",
"strictFunctionTypes",
"strictBindCallApply",
"strictPropertyInitialization",
"noImplicitThis",
"alwaysStrict",
"useUnknownInCatchVariables",
"strictBuiltinIteratorReturn",
];
const INCLUDE: Record<string, string[]> = {
"frontend/tsconfig.json": ["src"],
"frontend/tsconfig.node.json": [
"vite.config.ts",
"vitest.config.ts",
"reactPlugin.ts",
"devOnlyGuard.ts",
"playwright.config.ts",
"postcss.config.js",
],
"frontend/e2e/tsconfig.json": [".", "../playwright.config.ts"],
};
it("lists every weakening compiler flag", () => {
const found: [string, string][] = [];
for (const path of PROJECTS) {
const cfg = jsonc(path);
const opts = cfg.compilerOptions as Record<string, unknown>;
if (opts.skipLibCheck === true) found.push(["all three projects", "skipLibCheck"]);
for (const flag of STRICT_FAMILY) if (opts[flag] === false) found.push([path, flag]);
for (const key of ["extends", "exclude", "files"]) if (key in cfg) found.push([path, key]);
expect(cfg.include, path).toEqual(INCLUDE[path]);
}
expect(missingFrom(found)).toEqual([]);
});
it("type-checks every config file at the frontend root, or lists it", () => {
const checked = new Set(INCLUDE["frontend/tsconfig.node.json"]);
const roots = readdirSync(FRONTEND).filter((f) => /[.](ts|js|mjs|cjs)$/.test(f));
const found = roots
.filter((f) => !checked.has(f))
.map((f): [string, string] => ["config files", `${f} — not type-checked`]);
expect(missingFrom(found)).toEqual([]);
// a `.js` in `include` is dropped without a word unless allowJs, and read unchecked without
// checkJs (E32 review round 2)
const opts = jsonc("frontend/tsconfig.node.json").compilerOptions as Record<string, unknown>;
if ([...checked].some((f) => /[.][mc]?js$/.test(f)))
expect([opts.allowJs, opts.checkJs]).toEqual([true, true]);
});
// --- Ruff -----------------------------------------------------------------------------------------
// Ruff, pyright, pytest and knip are read as whitelists: a key this test does not know (an
// `extend-exclude`, a `lint.extend-ignore`, a narrowing `select`, a pyright `exclude`, a pytest
// `-m "not slow"`, a knip `rules`) fails until someone decides whether it narrows (E32 review —
// only `ignore` and the immutable calls used to be read). The TOML is PARSED, not scanned line by
// line: a `[lint.per-file-ignores]` table or a quoted key is a key path like any other (E32 review
// round 2 — a line regex saw neither).
const keyPaths = (o: Record<string, unknown>, prefix = ""): string[] =>
Object.entries(o).flatMap(([k, v]) =>
v !== null && typeof v === "object" && !Array.isArray(v)
? keyPaths(v as Record<string, unknown>, `${prefix}${k}.`)
: [`${prefix}${k}`],
);
it("lists every ruff ignore and immutable call, and knows every key", () => {
const cfg = parseToml(read("backend/ruff.toml"));
expect(keyPaths(cfg).sort()).toEqual(
[
"lint.extend-select",
"lint.flake8-bugbear.extend-immutable-calls",
"line-length",
"target-version",
].sort(),
);
const lint = cfg.lint as Record<string, unknown>;
// the selection on top of ruff's defaults (E, F) — dropping one narrows the lane
expect(lint["extend-select"]).toEqual([
"LOG",
"G",
"T20",
"TRY400",
"TRY401",
"S110",
"S112",
"BLE001",
"RUF100",
"PLW1510",
"B",
]);
const immutable = (lint["flake8-bugbear"] as Record<string, string[]>)[
"extend-immutable-calls"
]!;
const found = immutable.map((c): [string, string] => ["backend + scripts", c]);
expect(missingFrom(found)).toEqual([]);
});
// Every ruff lane passes `--config backend/ruff.toml`, which makes ruff ignore any other config
// file it would discover (measured: a nested `ruff.toml` selecting the `D` rules changed nothing).
// So that flag is what makes this file the whole configuration. The snapshot holds every CI line;
// these two are spelled out here because the reason is (round 3: a substring check let an extra
// `--config "lint.extend-ignore=…"` override through).
it("runs every CI ruff step on backend/ruff.toml and nothing else", () => {
const steps = read(".gitea/workflows/check.yml")
.split("\n")
.filter((l) => /\bruff\b/.test(l) && !l.trim().startsWith("#"))
.map((l) => l.trim());
expect(steps).toEqual([
"- name: ruff",
"run: python -m ruff check --config backend/ruff.toml backend scripts",
"- name: ruff format --check",
"run: python -m ruff format --check --config backend/ruff.toml backend scripts",
]);
});
// --- basedpyright ---------------------------------------------------------------------------------
const PYRIGHT_KEYS = [
"include",
"extraPaths",
"typeCheckingMode",
"pythonVersion",
"pythonPlatform",
];
it("lists the type-check mode, the baseline and every rule override, and knows every key", () => {
const cfg = jsonc("backend/pyrightconfig.json");
const found: [string, string][] = [];
if (cfg.typeCheckingMode !== "all" && cfg.typeCheckingMode !== "strict")
found.push(["backend/app + alembic", "typeCheckingMode"]);
found.push(["backend/app + alembic", "baseline.json"]);
const weak = [false, "none", "warning", "information", "hint"];
for (const [key, value] of Object.entries(cfg))
if (key.startsWith("report") && weak.includes(value as string | boolean))
found.push(["backend/app + alembic", key]);
expect(
Object.keys(cfg).filter((k) => !k.startsWith("report") && !PYRIGHT_KEYS.includes(k)),
).toEqual([]);
expect(cfg.include).toEqual(["app", "alembic"]); // tests/ out — its row says why
expect(missingFrom(found)).toEqual([]);
});
// --- pytest ---------------------------------------------------------------------------------------
// Held line by line, not key by key: pytest's ini reader also takes `key: value` and indented
// continuation lines (`addopts = -q` + ` -m "not slow"`), and a key regex saw neither (E32 review
// round 2). Every line that is not blank or a comment must be one of these, and all must be there.
it("keeps pytest strict: the whole tests/ tree, no selection, no warning filters, --strict-markers", () => {
const lines = read("backend/pytest.ini")
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => l.trim() !== "" && !/^[#;]/.test(l.trim()));
expect(lines).toEqual([
"[pytest]",
"testpaths = tests",
"pythonpath = .",
"addopts = -q -rs --strict-markers",
]);
});
// --- knip -----------------------------------------------------------------------------------------
it("lists every knip production exclusion and every other key, and runs both modes", () => {
const cfg = jsonc("frontend/knip.jsonc") as { project?: string[] } & Record<string, unknown>;
const found: [string, string][] = (cfg.project ?? [])
.filter((p) => p.startsWith("!"))
.map((p): [string, string] => ["--production", p]);
for (const key of Object.keys(cfg).filter(
(k) => !["$schema", "project", "playwright"].includes(k),
))
found.push(["knip", key]);
expect(missingFrom(found)).toEqual([]);
const pkg = JSON.parse(read("frontend/package.json")) as { scripts: Record<string, string> };
expect(pkg.scripts.knip).toBe("knip && knip --production");
// the positive pattern is the project itself: narrowing it drops files from both modes
expect((cfg.project ?? []).filter((p) => !p.startsWith("!"))).toEqual(["**/*.{ts,tsx,js}!"]);
// and knip.jsonc is the config knip reads: a knip.json (read first) or a package.json `knip` key
// (merged in) would narrow it unseen (E32 review round 3)
const KNIP_FILES = /^\.?knip(\.config)?\.(json|jsonc|js|ts|mjs|cjs|mts|cts|toml|ya?ml)$/;
expect(readdirSync(FRONTEND).filter((f) => KNIP_FILES.test(f))).toEqual(["knip.jsonc"]);
expect("knip" in pkg).toBe(false);
});
// --- accepted findings ----------------------------------------------------------------------------
it("accepts a CVE only with a statement, an expiry, and a row", () => {
const yaml = read(".trivyignore.yaml");
const entries = yaml
.split(/\n\s*- id:\s*/)
.slice(1)
.map((chunk) => ({
id: chunk.split("\n")[0]!.trim(),
statement: /\n\s*statement:\s*\S/.test(chunk),
expires: /\n\s*expired_at:\s*\d{4}-\d{2}-\d{2}/.test(chunk),
}));
expect(entries.filter((e) => !e.statement || !e.expires).map((e) => e.id)).toEqual([]);
const pipIgnores = read(".gitea/workflows/check.yml")
.split("\n")
.filter((l) => !l.trim().startsWith("#")) // the step's comment explains the flag; it is not one
.flatMap((l) => [...l.matchAll(/--ignore-vuln[ =](\S+)/g)].map((m) => m[1]!));
const found = [...entries.map((e) => e.id), ...pipIgnores].map((id): [string, string] => [
"accepted",
id,
]);
expect(missingFrom(found)).toEqual([]);
});
});