mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 19:02:17 +03:00
A global ratchet ("total ≤ baseline") reds an innocent PR whenever the base
drifted, and lets a PR that adds 10 violations pass as long as someone else
removed 11 — both happened this week. On pull_request events quality.yml now
passes --base-ref <PR base SHA> to check:complexity-ratchets and check:dead-code
(file-size already had it); in that mode the gate compares HEAD with the
merge-base RESTRICTED to the files the PR touched:
- blocking: violations / dead exports the PR added in files it changed
(complexityNewCode=, cognitiveComplexityNewCode=, deadExportsNewCode=)
- advisory: the global total vs the frozen baseline (re-frozen at release,
watched by the nightly headroom job)
scripts/check/newCodeMode.mjs holds the git side (merge-base, changed files,
throwaway `git worktree` of the base with node_modules linked — no stash, no
checkout) and the pure comparison helpers (13 unit tests). ESLint runs only on
the changed files in both trees (~20 s); knip runs twice (~70 s).
Exercised locally against the last 8 merges: complexity flagged
src/lib/credentialHealth/scheduler.ts (2→3, cognitive 1→2) and dead-code flagged
src/lib/resilience/settings.ts:CredentialHealthCheckSettings — findings the
global totals were hiding under the relaxed baselines.
workflow_dispatch, the release-green sweep and the headroom job have no PR base
and keep the absolute comparison. Docs: QUALITY_GATES.md → "New-code mode".
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
baseRefArg,
|
|
deadSymbolKeys,
|
|
diffNewCode,
|
|
filterScope,
|
|
newDeadSymbols,
|
|
perFileRuleCounts,
|
|
} from "../../../scripts/check/newCodeMode.mjs";
|
|
|
|
test("baseRefArg reads --base-ref <sha> and ignores a dangling flag", () => {
|
|
assert.equal(baseRefArg(["node", "x", "--base-ref", "abc123"]), "abc123");
|
|
assert.equal(baseRefArg(["node", "x", "--base-ref"]), null);
|
|
assert.equal(baseRefArg(["node", "x"]), null);
|
|
});
|
|
|
|
test("filterScope keeps only in-scope dirs/extensions, sorted and trimmed", () => {
|
|
const files = filterScope(
|
|
[
|
|
" src/a.ts",
|
|
"open-sse/b.tsx",
|
|
"tests/unit/c.test.ts",
|
|
"src/d.md",
|
|
"srcx/e.ts",
|
|
"",
|
|
"bin/f.mjs",
|
|
],
|
|
{ dirs: ["src", "open-sse", "bin"], exts: [".ts", ".tsx", ".mjs"] }
|
|
);
|
|
assert.deepEqual(files, ["bin/f.mjs", "open-sse/b.tsx", "src/a.ts"]);
|
|
});
|
|
|
|
test("perFileRuleCounts counts only the requested rules and relativizes absolute paths", () => {
|
|
const report = [
|
|
{
|
|
filePath: "/repo/src/a.ts",
|
|
messages: [
|
|
{ ruleId: "complexity" },
|
|
{ ruleId: "max-lines-per-function" },
|
|
{ ruleId: "no-unused-vars" },
|
|
],
|
|
},
|
|
{ filePath: "/repo/src/b.ts", messages: [{ ruleId: "sonarjs/cognitive-complexity" }] },
|
|
{ filePath: "src/c.ts", messages: [] },
|
|
];
|
|
const cyc = perFileRuleCounts(report, new Set(["complexity", "max-lines-per-function"]), "/repo");
|
|
assert.equal(cyc.get("src/a.ts"), 2);
|
|
assert.equal(cyc.get("src/b.ts"), 0);
|
|
assert.equal(cyc.get("src/c.ts"), 0);
|
|
const cog = perFileRuleCounts(report, new Set(["sonarjs/cognitive-complexity"]), "/repo");
|
|
assert.equal(cog.get("src/b.ts"), 1);
|
|
});
|
|
|
|
test("diffNewCode flags only changed files that grew; new files count from zero", () => {
|
|
const head = new Map([
|
|
["src/a.ts", 3],
|
|
["src/new.ts", 1],
|
|
["src/untouched.ts", 9],
|
|
]);
|
|
const base = new Map([
|
|
["src/a.ts", 3],
|
|
["src/untouched.ts", 2],
|
|
]);
|
|
const r = diffNewCode(head, base, ["src/a.ts", "src/new.ts"]);
|
|
assert.deepEqual(r.regressions, [{ file: "src/new.ts", base: 0, head: 1 }]);
|
|
assert.equal(r.head, 4);
|
|
assert.equal(r.base, 3);
|
|
assert.equal(r.delta, 1);
|
|
// a file that got better is not a regression
|
|
const better = diffNewCode(new Map([["src/a.ts", 1]]), new Map([["src/a.ts", 3]]), ["src/a.ts"]);
|
|
assert.deepEqual(better.regressions, []);
|
|
assert.equal(better.delta, -2);
|
|
});
|
|
|
|
test("deadSymbolKeys covers exports, types, namespace members and unused files", () => {
|
|
const keys = deadSymbolKeys({
|
|
issues: [
|
|
{
|
|
file: "src/a.ts",
|
|
exports: [{ name: "foo" }],
|
|
types: [{ name: "Bar" }],
|
|
nsExports: [{ name: "ns" }],
|
|
},
|
|
{ file: "src/dead.ts", files: ["src/dead.ts"] },
|
|
],
|
|
});
|
|
assert.deepEqual([...keys].sort(), [
|
|
"src/a.ts:Bar",
|
|
"src/a.ts:foo",
|
|
"src/a.ts:ns",
|
|
"src/dead.ts:<file>",
|
|
]);
|
|
assert.equal(deadSymbolKeys(null).size, 0);
|
|
});
|
|
|
|
test("newDeadSymbols reports only symbols that are new on HEAD, in touched files by default", () => {
|
|
const base = { issues: [{ file: "src/a.ts", exports: [{ name: "old" }] }] };
|
|
const head = {
|
|
issues: [
|
|
{ file: "src/a.ts", exports: [{ name: "old" }, { name: "fresh" }] },
|
|
{ file: "src/other.ts", exports: [{ name: "orphaned" }] },
|
|
],
|
|
};
|
|
assert.deepEqual(newDeadSymbols(head, base, ["src/a.ts"]), ["src/a.ts:fresh"]);
|
|
assert.deepEqual(newDeadSymbols(head, base, ["src/a.ts"], { includeUntouched: true }), [
|
|
"src/a.ts:fresh",
|
|
"src/other.ts:orphaned",
|
|
]);
|
|
assert.deepEqual(newDeadSymbols(base, head, ["src/a.ts"]), []);
|
|
});
|