From 685526d53b9b4346221d52658a790aabcfae3489 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 5 Aug 2026 14:10:59 +0200 Subject: [PATCH] Add a regression test for the oxlint gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A gate that checks nothing exits 0, which is indistinguishable from a clean run — that is why the two invocation bugs fixed in 91f46ff went 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: the b6ee4c8 and 9b0cb38 configurations, --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) --- .../domain/src/__tests__/lint-gate.test.ts | 14 ++++ scripts/check-lint-gate.mjs | 71 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 packages/domain/src/__tests__/lint-gate.test.ts create mode 100644 scripts/check-lint-gate.mjs diff --git a/packages/domain/src/__tests__/lint-gate.test.ts b/packages/domain/src/__tests__/lint-gate.test.ts new file mode 100644 index 0000000..17d8297 --- /dev/null +++ b/packages/domain/src/__tests__/lint-gate.test.ts @@ -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); + }); +}); diff --git a/scripts/check-lint-gate.mjs b/scripts/check-lint-gate.mjs new file mode 100644 index 0000000..8182e84 --- /dev/null +++ b/scripts/check-lint-gate.mjs @@ -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} 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} */ + 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; +}