diff --git a/changelog.d/fixes/6634-6634-testmasking-selfref.md b/changelog.d/fixes/6634-6634-testmasking-selfref.md new file mode 100644 index 0000000000..2982803f5a --- /dev/null +++ b/changelog.d/fixes/6634-6634-testmasking-selfref.md @@ -0,0 +1 @@ +- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634) diff --git a/scripts/check/check-test-masking.mjs b/scripts/check/check-test-masking.mjs index 3b5bf0e350..96f67e9d0b 100644 --- a/scripts/check/check-test-masking.mjs +++ b/scripts/check/check-test-masking.mjs @@ -319,6 +319,26 @@ export function partitionDeletedRenamed(nameStatusOutput) { * Os campos de skip e extTaut são opcionais (default 0) para compatibilidade * com chamadas legadas que só passam baseAsserts/headAsserts/baseTaut/headTaut. */ +/** + * (#6634) `check-test-masking.test.ts` legitimately embeds tautology-pattern string + * literals (`assert.ok(true)`, `expect(true).toBe(true)`, `assert.equal(1,1)`) as + * FIXTURES to exercise `countBareTautologies()`/`scanBareTautologies()` (#6404). The + * diff-based tautology counters (`countTautologies()`/`countExtendedTautologies()`) + * are dumb regex scans of raw source text with no awareness that a literal sits + * inside a fixture string rather than real assertion code, so any new fixture line + * self-trips a HARD "new tautology" flag on the gate's own regression-test file. + * Mirrors the exclusion `scanBareTautologies()` already applies for the same reason. + * + * The exclusion covers the whole `check-test-masking*` gate self-test family — not + * just `check-test-masking.test.ts` but sibling regression files such as + * `check-test-masking-selfref-6634.test.ts`, which likewise embed tautology-pattern + * literals as fixtures/documentation to prove this gate's own behavior. + */ +const SELF_TEST_FIXTURE_RE = /(^|\/)check-test-masking(-[\w-]+)?\.test\.tsx?$/; +function isSelfTestFixtureFile(file) { + return SELF_TEST_FIXTURE_RE.test(file); +} + export function evaluateMasking(perFile, assertReductionAllowlist = new Set()) { const flags = []; for (const f of perFile) { @@ -326,6 +346,7 @@ export function evaluateMasking(perFile, assertReductionAllowlist = new Set()) { const headSkips = f.headSkips ?? 0; const baseExtTaut = f.baseExtTaut ?? 0; const headExtTaut = f.headExtTaut ?? 0; + const isSelfTestFixture = isSelfTestFixtureFile(f.file); // The net-assert-REDUCTION signal can be allowlisted per file when the reduction is a // verified-legitimate refactor/field-removal (config/quality/test-masking-allowlist.json). @@ -334,13 +355,13 @@ export function evaluateMasking(perFile, assertReductionAllowlist = new Set()) { flags.push( `${f.file}: asserts ${f.baseAsserts} → ${f.headAsserts} (REMOÇÃO de ${f.baseAsserts - f.headAsserts} — enfraquecimento?)` ); - if (f.headTaut > f.baseTaut) + if (!isSelfTestFixture && f.headTaut > f.baseTaut) flags.push(`${f.file}: nova(s) ${f.headTaut - f.baseTaut} tautologia(s) assert.ok(true)`); if (headSkips > baseSkips) flags.push( `${f.file}: ${headSkips - baseSkips} novo(s) .skip/.todo/.only (asserts silenciados sem remoção)` ); - if (headExtTaut > baseExtTaut) + if (!isSelfTestFixture && headExtTaut > baseExtTaut) flags.push( `${f.file}: nova(s) ${headExtTaut - baseExtTaut} tautologia(s) estendida(s) (expect(true).toBe(true) / assert.equal(1,1))` ); @@ -374,7 +395,7 @@ export function scanBareTautologies(testFiles, readFile) { const read = readFile || ((f) => fs.readFileSync(f, "utf8")); const flags = []; for (const file of testFiles || []) { - if (file.endsWith("check-test-masking.test.ts")) continue; + if (isSelfTestFixtureFile(file)) continue; let src; try { src = read(file); diff --git a/scripts/quality/validate-release-green.mjs b/scripts/quality/validate-release-green.mjs index aecc88734a..db0933ed67 100644 --- a/scripts/quality/validate-release-green.mjs +++ b/scripts/quality/validate-release-green.mjs @@ -96,7 +96,9 @@ export function firstFailureLine(out) { .split("\n") .map((l) => l.trim()) .filter(Boolean); - const hit = lines.find((l) => /✖|not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l)); + const hit = lines.find((l) => + /✖|✗|not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l) + ); return (hit || lines[lines.length - 1] || "failed").slice(0, 200); } diff --git a/tests/unit/check-test-masking-selfref-6634.test.ts b/tests/unit/check-test-masking-selfref-6634.test.ts new file mode 100644 index 0000000000..6d91a2e7c2 --- /dev/null +++ b/tests/unit/check-test-masking-selfref-6634.test.ts @@ -0,0 +1,84 @@ +/** + * Regression test for issue #6634 ("release/v3.8.47 branch not green — nightly + * release-green found HARD failures"). The nightly's "Test-masking + * (weakened-assert guard)" HARD failure was a SELF-REFERENTIAL false positive: + * tests/unit/check-test-masking.test.ts legitimately embeds tautology-pattern + * string literals (e.g. `expect(true).toBe(true);`, `assert.equal(1, 1);`) as + * FIXTURES to exercise countBareTautologies()/scanBareTautologies() — the same + * literal text that the diff-based subcheck (evaluateMasking(), fed by + * countTautologies()/countExtendedTautologies()) treated as "new tautologies in + * the file itself" because those counters are dumb regex scans of raw source + * text, blind to "this is inside a fixture string, not real assertion code". + * + * scanBareTautologies() already special-cases this exact file + * (`if (file.endsWith("check-test-masking.test.ts")) continue;` in + * scripts/check/check-test-masking.mjs) for precisely this reason — this test + * asserts evaluateMasking() now applies the same exclusion for its diff-based + * tautology counters, using the real base(origin/main)/head(HEAD) diff of + * tests/unit/check-test-masking.test.ts. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; + +import { + countTautologies, + countExtendedTautologies, + evaluateMasking, +} from "../../scripts/check/check-test-masking.mjs"; + +const FILE = "tests/unit/check-test-masking.test.ts"; + +function git(args: string[]): string { + return execFileSync("git", args, { encoding: "utf8" }); +} + +test("#6634: check-test-masking.test.ts's own tautology fixtures must not self-flag as weakening", () => { + // origin/main predates the #6404 fixtures (countBareTautologies/scanBareTautologies + // tests) that legitimately embed tautology-pattern literals as string fixtures. + const baseSrc = git(["show", "origin/main:" + FILE]); + const headSrc = git(["show", "HEAD:" + FILE]); + + const perFile = [ + { + file: FILE, + baseAsserts: 0, // irrelevant to this assertion — only tautology counters matter + headAsserts: 0, + baseTaut: countTautologies(baseSrc), + headTaut: countTautologies(headSrc), + baseExtTaut: countExtendedTautologies(baseSrc), + headExtTaut: countExtendedTautologies(headSrc), + }, + ]; + + const flags = evaluateMasking(perFile, new Set()); + + assert.deepEqual( + flags, + [], + "check-test-masking.test.ts's own literal tautology fixtures (added for #6404) must be " + + "excluded from the diff-based weakening check the same way scanBareTautologies() already " + + "excludes this file from the absolute-floor scan — otherwise the gate's own regression " + + "test permanently self-flags as a HARD release-green failure whenever new fixtures are added." + ); +}); + +test("#6634: unrelated files still get flagged for genuinely new tautologies (guard is file-scoped, not global)", () => { + const perFile = [ + { + file: "tests/unit/some-other-file.test.ts", + baseAsserts: 5, + headAsserts: 5, + baseTaut: 0, + headTaut: 1, + baseExtTaut: 0, + headExtTaut: 1, + }, + ]; + + const flags = evaluateMasking(perFile, new Set()); + + assert.equal(flags.length, 2, "a non-fixture file must still trip both tautology signals"); + assert.match(flags[0], /nova\(s\) 1 tautologia\(s\) assert\.ok\(true\)/); + assert.match(flags[1], /nova\(s\) 1 tautologia\(s\) estendida/); +}); diff --git a/tests/unit/check-test-masking.test.ts b/tests/unit/check-test-masking.test.ts index 5581d88b15..5579bd1cf8 100644 --- a/tests/unit/check-test-masking.test.ts +++ b/tests/unit/check-test-masking.test.ts @@ -330,6 +330,21 @@ test("scanBareTautologies: excludes check-test-masking.test.ts itself", () => { assert.deepEqual(scanBareTautologies(files, read), []); }); +test("scanBareTautologies: excludes sibling gate self-test files (#6634 selfref regression)", () => { + // The gate's own regression files (e.g. check-test-masking-selfref-6634.test.ts) + // embed tautology-pattern literals as fixtures/documentation — the family-wide + // exclusion must cover them too, not only check-test-masking.test.ts itself. + const files = ["tests/unit/check-test-masking-selfref-6634.test.ts"]; + const read = () => `assert.equal(1, 1);`; + assert.deepEqual(scanBareTautologies(files, read), []); +}); + +test("scanBareTautologies: a non-family file with the pattern is still flagged (exclusion is scoped)", () => { + const files = ["tests/unit/some-unrelated.test.ts"]; + const read = () => `assert.equal(1, 1);`; + assert.equal(scanBareTautologies(files, read).length, 1); +}); + test("scanBareTautologies: skips unreadable files instead of throwing", () => { const files = ["tests/unit/does-not-exist.test.ts"]; const read = () => {