The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
604 lines
27 KiB
TypeScript
604 lines
27 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. 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: ESLint's effective config per scope, 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 gate line. A comparison against everything has no
|
|
// shape to miss.)
|
|
//
|
|
// A scope is named by PATH (`scopeOf`), and every file of a scope must get the same effective config.
|
|
// So a new file in a known place changes nothing (the snapshot once listed every file, and 31% of
|
|
// commits regenerated it), while a glob that moves a file into another config splits its scope and
|
|
// fails below, and a scope whose rules change is a snapshot diff.
|
|
//
|
|
// Regenerate: GATE_SNAPSHOT=update npx vitest run src/lib/gates.contract.test.ts
|
|
// then read the diff like any gate change. A new kind of file (a new config or a new place) needs a
|
|
// line in `scopeOf` and, when it narrows anything, its register rows.
|
|
|
|
const SNAPSHOT = "frontend/src/lib/gates.snapshot.json";
|
|
type EslintConfig = {
|
|
rules: Record<string, unknown[]>;
|
|
linterOptions: Record<string, unknown>;
|
|
languageOptions: Record<string, unknown>;
|
|
plugins: string[];
|
|
settings: unknown;
|
|
};
|
|
type Snapshot = {
|
|
eslint: Record<string, EslintConfig>;
|
|
lanes: Record<string, unknown>;
|
|
};
|
|
|
|
/** The register scope a code file belongs to, by its path (relative to frontend/). */
|
|
function scopeOf(file: string): string {
|
|
if (!file.includes("/")) return /[.][mc]?js$/.test(file) ? "js configs" : "ts configs";
|
|
if (file.startsWith("e2e/")) return "e2e";
|
|
if (file.startsWith("src/design-lab/")) return "design-lab";
|
|
// the network owners (NETWORK_OWNERS in eslint.config.js)
|
|
if (file === "src/lib/http.ts") return "lib/http.ts";
|
|
if (file === "src/lib/api/core.ts") return "lib/api/core.ts";
|
|
const test = /[.]test[.]tsx?$/.test(file);
|
|
if (file.startsWith("src/components/ui/")) return test ? "ui tests" : "ui";
|
|
if (file.startsWith("src/lib/")) return test ? "lib tests" : "src/lib";
|
|
return test ? "src tests" : "src";
|
|
}
|
|
|
|
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 is reported as 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 a dozen 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 Omit<Snapshot, "eslint"> & {
|
|
eslint: Record<string, Stored>;
|
|
};
|
|
return canonical({ ...s, eslint: fromStored(s.eslint) });
|
|
};
|
|
|
|
type EslintActual = {
|
|
configs: Record<string, EslintConfig>;
|
|
/** the files no config lints */
|
|
notLinted: string[];
|
|
/** `scope: file` for each file whose config is not its scope's (the scope's majority) */
|
|
strays: string[];
|
|
};
|
|
async function eslintActual(): Promise<EslintActual> {
|
|
const eslint = new ESLint({ cwd: FRONTEND });
|
|
const byScope = new Map<string, 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 scope = scopeOf(file);
|
|
const groups =
|
|
byScope.get(scope) ?? new Map<string, { config: EslintConfig; files: string[] }>();
|
|
const group = groups.get(key) ?? { config, files: [] };
|
|
group.files.push(file);
|
|
groups.set(key, group);
|
|
byScope.set(scope, groups);
|
|
}
|
|
const configs: Record<string, EslintConfig> = {};
|
|
const strays: string[] = [];
|
|
for (const [scope, groups] of byScope) {
|
|
const [first, ...rest] = [...groups.values()].sort((a, b) => b.files.length - a.files.length);
|
|
configs[scope] = first!.config;
|
|
for (const g of rest) strays.push(...g.files.map((f) => `${scope}: ${f}`));
|
|
}
|
|
return { configs: canonical(configs), notLinted, strays };
|
|
}
|
|
|
|
// 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 = () => ({
|
|
"scripts/check.sh": meaningful("scripts/check.sh"),
|
|
"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<EslintActual> | undefined;
|
|
const linted = () => (actual ??= eslintActual());
|
|
const current = async (): Promise<Snapshot> =>
|
|
canonical({ eslint: (await linted()).configs, 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, notLinted }: EslintActual): [string, string][] {
|
|
const found: [string, string][] = notLinted.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 scope 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: toStored(now.eslint) };
|
|
writeFileSync(path, await format(JSON.stringify(sortKeys(stored)), options));
|
|
return;
|
|
}
|
|
expect(now).toEqual(committed());
|
|
}, 120_000);
|
|
|
|
// A glob that moves one file into another config (a product file into the test config) leaves its
|
|
// scope's config as it was, so the snapshot alone would not see it: the file now differs from the
|
|
// rest of its scope, and that is what fails here.
|
|
it("gives every file of a scope the same config", async () => {
|
|
expect((await linted()).strays).toEqual([]);
|
|
}, 120_000);
|
|
});
|
|
|
|
describe("the gate register (docs/gates.md)", () => {
|
|
it("lists every ESLint rule narrowed, by scope", async () => {
|
|
expect(missingFrom(eslintNarrowings(await linted()))).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.
|
|
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
|
|
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 (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 (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 line of
|
|
// the gate script; these two are spelled out because the reason is (a substring check once let
|
|
// an extra `--config "lint.extend-ignore=…"` override through).
|
|
it("runs both ruff lanes on backend/ruff.toml and nothing else", () => {
|
|
const lanes = read("scripts/check.sh")
|
|
.split("\n")
|
|
.filter((l) => /python -m ruff (check|format)/.test(l))
|
|
.map((l) => l.trim());
|
|
expect(lanes).toEqual([
|
|
"ruff_check() { ruff_pinned && python -m ruff check --config backend/ruff.toml backend scripts; }",
|
|
"ruff_format() { ruff_pinned && 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.
|
|
// 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 exclusion and every other key", () => {
|
|
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] => ["knip", 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");
|
|
// the positive pattern is the project itself: narrowing it drops files from the lane
|
|
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
|
|
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("scripts/check.sh")
|
|
.split("\n")
|
|
.filter((l) => !l.trim().startsWith("#")) // the lane'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([]);
|
|
});
|
|
});
|