ci: make quarantined test debt bounded and executable (#14426)

Rebased onto the tip after #14420 landed (clean merge, no conflicts). Independent validation: focused tests green (quarantine-contract/test-discovery/vitest-exclusions for #14426: 25/25; workflow-audit/check-workflows/scanner-installer for #14497: 48/48), file-size, changelog-integrity, complexity, test-discovery, env-doc-sync and typecheck:core green; actionlint shows no findings beyond the 32 already on the tip.
This commit is contained in:
Diego Rodrigues de Sa e Souza 2026-10-01 21:55:25 -03:00 • committed by GitHub
parent 7476346a6f
commit bd0e53e476
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 379 additions and 26 deletions

60
.github/workflows/test-quarantine.yml vendored Normal file
View file

@ -0,0 +1,60 @@
name: Quarantined Tests (debt, not admission)
on:
schedule:
- cron: "17 5 * * *"
workflow_dispatch:
permissions:
contents: read
issues: read
concurrency:
group: quarantine-${{ github.ref }}
cancel-in-progress: false
jobs:
quarantine:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version-file: .node-version
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Read current tracking issue states
id: inventory
env:
GH_TOKEN: ${{ github.token }}
run: |
node --input-type=module <<'JS'
import { readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
const inventory = JSON.parse(readFileSync('config/quality/vitest-exclusions.json', 'utf8'));
const states = {};
for (const issue of new Set(inventory.excluded.map(entry => entry.issue))) {
if (!/^#[1-9]\d*$/.test(issue)) throw new Error('Invalid issue reference');
const value = JSON.parse(execFileSync('gh', ['api', `repos/${process.env.GITHUB_REPOSITORY}/issues/${issue.slice(1)}`], { encoding: 'utf8' }));
states[issue] = value.state;
}
mkdirSync('.artifacts/quarantine', { recursive: true });
writeFileSync('.artifacts/quarantine/issue-states.json', JSON.stringify(states, null, 2));
appendFileSync(process.env.GITHUB_OUTPUT, `count=${inventory.excluded.length}\n`);
JS
- name: Validate inventory lifetime and tracker
run: node scripts/check/check-vitest-exclusions.mjs --issue-states .artifacts/quarantine/issue-states.json
- name: Execute quarantined tests
if: ${{ !cancelled() && steps.inventory.outputs.count != '' && steps.inventory.outputs.count != '0' }}
run: npx --no-install vitest run --config vitest.quarantine.config.ts --reporter=default --reporter=json --outputFile.json=.artifacts/quarantine/results.json
- name: Preserve measured failures and issue evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: quarantine-${{ github.sha }}-${{ github.run_attempt }}
path: .artifacts/quarantine/
if-no-files-found: error
retention-days: 14

View file

@ -22,13 +22,14 @@
// --update regrava o baseline com o estado atual (use só para REMOVER religados;
// adições novas devem ser corrigidas, não congeladas — esse é o ponto do gate).
//
// Limitações documentadas (v1):
// - `exclude` de arquivo individual em vitest configs não é modelado (1 caso hoje:
// providerDiversity.test.ts — coletado pelo include, deliberadamente excluído).
// Limitações documentadas:
// - Vitest exclude deve ser literal; expressões dinâmicas falham fechadas.
// - Quarentena tem coletor separado e nunca conta como execução ativa/saudável.
// - @omniroute/* ficam fora do walk (têm CI próprio: opencode-*-ci.yml).
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { parseVitestExcludes, validateQuarantine } from "../quality/quarantine-contract.mjs";
const ROOT = process.cwd();
const BASELINE_PATH = path.resolve(
@ -205,8 +206,20 @@ export function globToRegExp(glob) {
/** Arquivos de teste não casados por NENHUM glob de collector (ordem preservada). */
export function findOrphans(files, globs) {
const regexes = globs.map(globToRegExp);
return files.filter((f) => !regexes.some((re) => re.test(f)));
const collectors = globs.map((entry) => {
const collector = typeof entry === "string" ? { glob: entry } : entry;
return {
include: globToRegExp(collector.glob),
exclude: (collector.exclude ?? []).map(globToRegExp),
};
});
return files.filter(
(file) =>
!collectors.some(
(collector) =>
collector.include.test(file) && !collector.exclude.some((exclude) => exclude.test(file))
)
);
}
/**
@ -279,10 +292,30 @@ function main() {
// 2) órfãos vs baseline
const files = collectTestFiles();
const orphans = findOrphans(
files,
COLLECTORS.map((c) => c.glob)
const modeled = COLLECTORS.map((collector) => {
const config = collector.sources.find((source) => /^vitest.*\.config\.ts$/.test(source));
return { ...collector, exclude: config ? parseVitestExcludes(contents[config]) : [] };
});
const inventory = JSON.parse(
fs.readFileSync(path.join(ROOT, "config/quality/vitest-exclusions.json"), "utf8")
);
const quarantineErrors = validateQuarantine(inventory.excluded);
const quarantineConfig = fs.readFileSync(path.join(ROOT, "vitest.quarantine.config.ts"), "utf8");
const quarantineWorkflow = fs.readFileSync(
path.join(ROOT, ".github/workflows/test-quarantine.yml"),
"utf8"
);
if (
!quarantineConfig.includes("inventory.excluded.map") ||
!quarantineWorkflow.includes("vitest run --config vitest.quarantine.config.ts")
) {
quarantineErrors.push("quarantine execution wiring missing");
}
if (quarantineErrors.length) drift.push(...quarantineErrors);
const quarantined = new Set(inventory.excluded.map((entry) => entry.file));
const inactive = findOrphans(files, modeled);
// Only a valid, wired quarantine may account for a non-active file.
const orphans = inactive.filter((file) => quarantineErrors.length || !quarantined.has(file));
if (!fs.existsSync(BASELINE_PATH) && !UPDATE) {
console.error(
`[test-discovery] FAIL — ${path.basename(BASELINE_PATH)} ausente. Bootstrap:\n` +
@ -299,6 +332,12 @@ function main() {
};
const { newOrphans, stale } = evaluateAgainstBaseline(orphans, baseline.orphans || []);
if (UPDATE && newOrphans.length > 0) {
console.error(
`[test-discovery] FAIL — --update cannot freeze ${newOrphans.length} new orphan(s); wire their runner instead.`
);
process.exit(1);
}
if (UPDATE && drift.length === 0) {
baseline.orphans = orphans;
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2) + "\n");
@ -326,7 +365,7 @@ function main() {
process.exit(1);
}
console.log(
`[test-discovery] OK — ${files.length} arquivos de teste, ${COLLECTORS.length} collectors, ${(baseline.orphans || []).length} órfão(s) congelado(s) (dívida rastreada, só decresce)`
`[test-discovery] OK — ${files.length} arquivos de teste, ${COLLECTORS.length} collectors, ${quarantined.size} arquivo(s) inventariado(s) em quarentena (não é PASS ativo), ${(baseline.orphans || []).length} órfão(s) congelado(s) (dívida rastreada, só decresce)`
);
}

View file

@ -24,11 +24,10 @@
* reviewable diff in a dedicated file instead of one more line lost in a 60-entry
* array.
*
* What it deliberately does NOT do: re-run the excluded tests to see whether they pass
* again. That costs ~10 minutes and belongs in a periodic job, not in a per-PR gate. The
* inventory records the measured status and the date so a reader knows how stale it is.
* Owner, measured result and bounded expiry are mandatory. The separate quarantine
* workflow re-runs the inventory and verifies tracking issues against live GitHub.
*
* Standard tooling exclusions (`node_modules/**`, glob patterns, the live-server E2E specs
* Standard tooling exclusions (`node_modules/**`, the live-server E2E specs
* that have their own runner) are exempt — they are configuration, not debt.
*
* Usage:
@ -40,6 +39,7 @@ import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { validateQuarantine } from "../quality/quarantine-contract.mjs";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(SCRIPT_DIR, "..", "..");
@ -91,7 +91,12 @@ export function findViolations(entries, exists, inventory) {
const seen = new Set();
for (const { pattern, comment } of entries) {
if (EXEMPT.has(pattern) || pattern.includes("*")) continue;
if (EXEMPT.has(pattern)) continue;
// An arbitrary glob could hide hundreds of tests without an inventory entry.
if (/[*?\[\]{}]/.test(pattern)) {
untracked.push(pattern);
continue;
}
if (!exists(pattern)) continue; // a stale path excludes nothing
seen.add(pattern);
if (!/#\d+/.test(comment)) unreferenced.push(pattern);
@ -105,9 +110,14 @@ export function findViolations(entries, exists, inventory) {
function main() {
const json = process.argv.includes("--json");
const entries = parseExclusions(fs.readFileSync(CONFIG, "utf8"));
const inventory = fs.existsSync(INVENTORY)
? JSON.parse(fs.readFileSync(INVENTORY, "utf8")).excluded.map((e) => e.file)
: [];
const recorded = fs.existsSync(INVENTORY)
? JSON.parse(fs.readFileSync(INVENTORY, "utf8")).excluded
: undefined;
const inventory = (recorded ?? []).map((e) => e.file);
const issueArg = process.argv.indexOf("--issue-states");
const issueStates =
issueArg === -1 ? undefined : JSON.parse(fs.readFileSync(process.argv[issueArg + 1], "utf8"));
const metadataErrors = validateQuarantine(recorded, { issueStates });
const { unreferenced, untracked, orphaned } = findViolations(
entries,
@ -116,13 +126,16 @@ function main() {
);
if (json) {
console.log(JSON.stringify({ unreferenced, untracked, orphaned }, null, 2));
console.log(JSON.stringify({ unreferenced, untracked, orphaned, metadataErrors }, null, 2));
process.exitCode =
unreferenced.length + untracked.length + orphaned.length + metadataErrors.length ? 1 : 0;
return;
}
const failed = unreferenced.length + untracked.length + orphaned.length;
const failed = unreferenced.length + untracked.length + orphaned.length + metadataErrors.length;
if (!failed) {
console.log(
`[vitest-exclusions] OK — ${inventory.length} excluded file(s), each tracked and referenced.`
`[vitest-exclusions] OK — ${inventory.length} excluded file(s), owned, tracked and unexpired; live issue state ${issueStates ? "verified" : "not checked (periodic workflow)"}.`
);
return;
}
@ -150,6 +163,7 @@ function main() {
for (const p of orphaned) console.error(` ✗ ${p}`);
console.error(" The test runs again — drop it from the inventory.");
}
for (const error of metadataErrors) console.error(`[vitest-exclusions] FAIL — ${error}`);
process.exit(1);
}

View file

@ -0,0 +1,76 @@
import ts from "typescript";
// Parse configuration without evaluating arbitrary imports or executing its plugins.
// Dynamic exclude expressions require an explicit model update, never a silent [].
export function parseVitestExcludes(source) {
const file = ts.createSourceFile(
"vitest.config.ts",
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS
);
if (file.parseDiagnostics.length) throw new Error("invalid Vitest configuration syntax");
const found = [];
function visit(node) {
if (ts.isPropertyAssignment(node) && node.name.getText(file) === "exclude") {
if (
!ts.isArrayLiteralExpression(node.initializer) ||
!node.initializer.elements.every(ts.isStringLiteral)
) {
throw new Error("Vitest exclude must be a literal string array for discovery");
}
found.push(node.initializer.elements.map((item) => item.text));
}
ts.forEachChild(node, visit);
}
visit(file);
if (found.length !== 1) throw new Error("expected exactly one Vitest exclude array");
return found[0];
}
function dateValue(value) {
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return NaN;
const time = Date.parse(`${value}T00:00:00Z`);
return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === value ? time : NaN;
}
export function validateQuarantine(entries, { now = new Date(), issueStates } = {}) {
const errors = [];
if (!Array.isArray(entries)) return ["quarantine inventory must be an array"];
const seen = new Set();
for (const entry of entries) {
const file = entry?.file;
const label = typeof file === "string" ? file : "invalid entry";
if (
typeof file !== "string" ||
!/^(tests|src|open-sse)\//.test(file) ||
file.split("/").some((part) => !part || part === "." || part === "..") ||
/[\\*?\[\]{}]/.test(file) ||
!/\.(test|spec)\.[cm]?[jt]sx?$/.test(file)
) {
errors.push(`${label}: expected one repository-relative test file`);
}
if (seen.has(file)) errors.push(`${label}: duplicate quarantine entry`);
seen.add(file);
if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/.test(entry?.owner ?? ""))
errors.push(`${label}: missing owner`);
if (!/^#[1-9]\d*$/.test(entry?.issue ?? "")) errors.push(`${label}: missing issue`);
if (typeof entry?.status !== "string" || !entry.status.trim())
errors.push(`${label}: missing measured result`);
const measured = dateValue(entry?.measured);
const expires = dateValue(entry?.expires);
if (!Number.isFinite(measured) || measured > now.getTime())
errors.push(`${label}: invalid measured date`);
if (
!Number.isFinite(expires) ||
expires <= now.getTime() ||
expires <= measured ||
expires - measured > 30 * 86_400_000
)
errors.push(`${label}: expired or invalid expiry (maximum 30 days from measurement)`);
if (issueStates !== undefined && issueStates[entry?.issue]?.toLowerCase() !== "open")
errors.push(`${label}: tracking issue is not confirmed open`);
}
return errors;
}

View file

@ -50,6 +50,13 @@ test("findOrphans: múltiplos collectors — basta UM casar", () => {
assert.deepEqual(findOrphans(files, globs), []);
});
test("findOrphans: exclude wins within its collector, but not over another runner", () => {
const file = "tests/unit/ui/parked.test.tsx";
const ui = { glob: "tests/unit/**/*.test.tsx", exclude: ["tests/unit/ui/**"] };
assert.deepEqual(findOrphans([file], [ui]), [file]);
assert.deepEqual(findOrphans([file], [ui, { glob: file, exclude: [] }]), []);
});
test("evaluateAgainstBaseline: órfão novo é flagado; órfão congelado passa", () => {
const { newOrphans, stale } = evaluateAgainstBaseline(
["tests/unit/novo/a.test.ts", "tests/unit/velho/b.test.ts"],
@ -60,10 +67,7 @@ test("evaluateAgainstBaseline: órfão novo é flagado; órfão congelado passa"
});
test("evaluateAgainstBaseline: entrada congelada que deixou de ser órfã é STALE (remova)", () => {
const { newOrphans, stale } = evaluateAgainstBaseline(
[],
["tests/unit/religado/c.test.ts"]
);
const { newOrphans, stale } = evaluateAgainstBaseline([], ["tests/unit/religado/c.test.ts"]);
assert.deepEqual(newOrphans, []);
assert.deepEqual(stale, ["tests/unit/religado/c.test.ts"]);
});

View file

@ -59,12 +59,16 @@ test("an inventory entry that is no longer excluded fails the gate", () => {
assert.deepEqual(v.orphaned, ["tests/revived.test.ts"]);
});
test("tooling exclusions and globs are exempt, and a stale path is ignored", () => {
test("only named tooling exclusions are exempt, and a stale path is ignored", () => {
const entries = [
{ pattern: "node_modules/**", comment: "," },
{ pattern: "tests/unit/**/*.test.tsx", comment: "," },
{ pattern: "tests/deleted.test.ts", comment: "," },
];
const v = findViolations(entries, (p) => p !== "tests/deleted.test.ts", []);
assert.deepEqual(v, { unreferenced: [], untracked: [], orphaned: [] });
});
test("an arbitrary glob cannot hide tests outside the inventory", () => {
const v = findViolations([{ pattern: "tests/unit/**/*.test.tsx", comment: "," }], () => true, []);
assert.deepEqual(v.untracked, ["tests/unit/**/*.test.tsx"]);
});

View file

@ -0,0 +1,139 @@
import assert from "node:assert/strict";
import { readFileSync, mkdtempSync, writeFileSync, rmSync, mkdirSync, copyFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import { load } from "js-yaml";
import {
parseVitestExcludes,
validateQuarantine,
} from "../../scripts/quality/quarantine-contract.mjs";
import { COLLECTORS } from "../../scripts/check/check-test-discovery.mjs";
const entry = {
file: "tests/unit/ui/example.test.tsx",
issue: "#13204",
owner: "diegosouzapw",
measured: "2026-09-10",
expires: "2026-09-24",
status: "1 failed",
};
const now = new Date("2026-09-22T01:00:00Z");
test("discovery --update cannot silently freeze newly orphaned tests", () => {
const dir = mkdtempSync(join(tmpdir(), "qg-discovery-baseline-"));
const baseline = join(dir, "baseline.json");
const original = JSON.stringify({ orphans: [] });
writeFileSync(baseline, original);
try {
const root = new URL("../../", import.meta.url);
const fixtureSources = new Set([
...COLLECTORS.flatMap((collector) => collector.sources),
"config/quality/vitest-exclusions.json",
"vitest.quarantine.config.ts",
".github/workflows/test-quarantine.yml",
]);
for (const source of fixtureSources) {
mkdirSync(dirname(join(dir, source)), { recursive: true });
copyFileSync(new URL(source, root), join(dir, source));
}
writeFileSync(
join(dir, "config/quality/vitest-exclusions.json"),
JSON.stringify({ excluded: [] })
);
mkdirSync(join(dir, "tests/unassigned"), { recursive: true });
writeFileSync(
join(dir, "tests/unassigned/orphan.test.ts"),
"// deliberately uncollected fixture\n"
);
const result = spawnSync(
process.execPath,
[
fileURLToPath(new URL("scripts/check/check-test-discovery.mjs", root)),
"--update",
"--baseline",
baseline,
],
{
cwd: dir,
encoding: "utf8",
timeout: 60_000,
}
);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stderr, /new orphan|NOVO|novo/i);
assert.equal(readFileSync(baseline, "utf8"), original);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("zero quarantined files is valid debt retirement, not a requirement to retain failures", () => {
assert.deepEqual(validateQuarantine([], { now }), []);
});
test("discovery parses actual inline/multiline Vitest excludes and rejects dynamic ones", () => {
assert.deepEqual(
parseVitestExcludes('export default { test: { exclude: ["a", /* comment */ "b"] } }'),
["a", "b"]
);
assert.throws(
() => parseVitestExcludes("export default { test: { exclude: [...hidden] } }"),
/literal/
);
assert.throws(
() => parseVitestExcludes("export default { test: { exclude: external } }"),
/literal/
);
assert.throws(() => parseVitestExcludes("export default { test: {} }"), /exclude/);
});
test("quarantine requires bounded ownership, measured evidence, and an open issue when checked", () => {
assert.deepEqual(validateQuarantine([entry], { now, issueStates: { "#13204": "open" } }), []);
for (const field of ["owner", "issue", "measured", "expires", "status"]) {
const invalid = { ...entry };
delete invalid[field];
assert.ok(validateQuarantine([invalid], { now }).length, field);
}
assert.ok(validateQuarantine([entry], { now, issueStates: {} }).length);
assert.ok(validateQuarantine([entry], { now, issueStates: { "#13204": "closed" } }).length);
});
test("expired, future, duplicate, escaped and broad exclusions fail closed", () => {
for (const patch of [
{ expires: "2026-09-21" },
{ expires: "2026-12-01" },
{ measured: "2026-09-23" },
{ measured: "2026-02-31" },
{ file: "../outside.test.ts" },
{ file: "tests/**" },
])
assert.ok(validateQuarantine([{ ...entry, ...patch }], { now }).length, JSON.stringify(patch));
assert.ok(validateQuarantine([entry, entry], { now }).length);
});
test("quarantine workflow executes the inventory and keeps failure/artifacts visible", () => {
const root = new URL("../../", import.meta.url);
const workflow = load(
readFileSync(new URL(".github/workflows/test-quarantine.yml", root), "utf8")
);
assert.ok(workflow.on.schedule.length);
assert.ok(Object.hasOwn(workflow.on, "workflow_dispatch"));
const job = workflow.jobs.quarantine;
assert.equal(job["runs-on"], "ubuntu-latest");
assert.equal(job["continue-on-error"], undefined);
assert.ok(job.steps.some((s) => s.run?.includes("--issue-states")));
const run = job.steps.find((s) => s.run?.includes("vitest.quarantine.config.ts"));
assert.ok(run);
assert.equal(run["continue-on-error"], undefined);
assert.ok(!run.run.includes("|| true"));
assert.ok(
job.steps.some((s) => s.uses?.startsWith("actions/upload-artifact@") && s.if === "always()")
);
const config = readFileSync(new URL("vitest.quarantine.config.ts", root), "utf8");
assert.match(config, /inventory\.excluded\.map/);
assert.match(config, /exclude:\s*\[\]/);
assert.match(config, /passWithNoTests:\s*false/);
});

View file

@ -0,0 +1,17 @@
import { defineConfig } from "vitest/config";
import base from "./vitest.config";
import inventory from "./config/quality/vitest-exclusions.json";
// Same transforms, aliases and setup as the UI rail; only the inventory is selected.
// Failure remains visible in its dedicated workflow, never relabeled as a green test.
export default defineConfig({
...base,
test: {
...base.test,
include: inventory.excluded.map((entry) => entry.file),
exclude: [],
passWithNoTests: false,
maxWorkers: 2,
fileParallelism: false,
},
});