Add a regression test for the oxlint gate
A gate that checks nothing exits 0, which is indistinguishable from a clean run — that is why the two invocation bugs fixed in91f46ffwent unnoticed across 49 commits. The check runs the configured oxlint command with one rule forced to warn and requires a non-zero exit. That single probe covers a missing --deny-warnings, a command that discovers no files, a broken tsconfig path, and an upstream flag rename. Two things the probe cannot see get their own assertions: that every gate invokes `pnpm oxlint` with nothing appended, and that --type-aware is still enabled, since the canary rule is not type-aware. Verified against four reconstructed states: theb6ee4c8and9b0cb38configurations, --type-aware removed, and a tsconfig path that matches no files. Each fails; the current configuration passes. Follows the existing check-layer-boundaries.mjs pattern — an exported function driven by a Vitest case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { checkLintGate } from "../../../../scripts/check-lint-gate.mjs";
|
||||||
|
|
||||||
|
describe("lint gate", () => {
|
||||||
|
it("fails on warnings instead of silently passing", () => {
|
||||||
|
const violations = checkLintGate();
|
||||||
|
if (violations.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Lint gate integrity violations:\n ${violations.join("\n ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(violations).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
const ROOT = new URL("..", import.meta.url).pathname.replace(/\/$/, "");
|
||||||
|
const RUN_LINE_RE = /^\s*run:\s*(.+?)\s*$/;
|
||||||
|
|
||||||
|
/** @param {string} path */
|
||||||
|
const read = (path) => readFileSync(join(ROOT, path), "utf-8");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gates must invoke `pnpm oxlint` with no extra arguments: pnpm forwards them
|
||||||
|
* after `--`, where oxlint reads them as file paths and lints nothing.
|
||||||
|
* @param {Record<string, string>} scripts
|
||||||
|
*/
|
||||||
|
function findGatesWithOwnFlags(scripts) {
|
||||||
|
const commands = Object.entries(scripts)
|
||||||
|
.filter(([name]) => name !== "oxlint")
|
||||||
|
.flatMap(([name, body]) =>
|
||||||
|
body.split("&&").map((c) => [`scripts.${name}`, c.trim()]),
|
||||||
|
);
|
||||||
|
for (const line of read("lefthook.yml").split("\n")) {
|
||||||
|
const command = RUN_LINE_RE.exec(line)?.[1];
|
||||||
|
if (command) commands.push(["lefthook.yml", command]);
|
||||||
|
}
|
||||||
|
return commands
|
||||||
|
.filter(([, c]) => c.includes("oxlint") && c !== "pnpm oxlint")
|
||||||
|
.map(([source, c]) => `${source} runs "${c}" instead of "pnpm oxlint"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the configured command with one rule forced to warn. A working gate
|
||||||
|
* exits non-zero; exiting 0 means it lints nothing, or does not fail on
|
||||||
|
* warnings.
|
||||||
|
* @param {string} oxlintScript
|
||||||
|
*/
|
||||||
|
function failsOnWarnings(oxlintScript) {
|
||||||
|
const args = [...oxlintScript.split(" ").slice(1), "-W", "no-console"];
|
||||||
|
try {
|
||||||
|
execFileSync(join(ROOT, "node_modules/.bin/oxlint"), args, {
|
||||||
|
cwd: ROOT,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports ways the oxlint gate could pass without checking anything, which is
|
||||||
|
* indistinguishable from a clean run.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function checkLintGate() {
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
const scripts = JSON.parse(read("package.json")).scripts;
|
||||||
|
const violations = findGatesWithOwnFlags(scripts);
|
||||||
|
|
||||||
|
// The canary rule below is not type-aware, so the probe alone cannot
|
||||||
|
// detect type-aware rules being switched off.
|
||||||
|
if (!scripts.oxlint.includes("--type-aware")) {
|
||||||
|
violations.push("scripts.oxlint is missing --type-aware");
|
||||||
|
}
|
||||||
|
if (!failsOnWarnings(scripts.oxlint)) {
|
||||||
|
violations.push(
|
||||||
|
`scripts.oxlint exits 0 despite no-console violations: "${scripts.oxlint}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user