mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-10-19 05:20:05 +00:00
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:
parent
7476346a6f
commit
bd0e53e476
8 changed files with 379 additions and 26 deletions
60
.github/workflows/test-quarantine.yml
vendored
Normal file
60
.github/workflows/test-quarantine.yml
vendored
Normal 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
|
||||
|
|
@ -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)`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
76
scripts/quality/quarantine-contract.mjs
Normal file
76
scripts/quality/quarantine-contract.mjs
Normal 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;
|
||||
}
|
||||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
|
|
|
|||
139
tests/unit/quarantine-contract.test.mjs
Normal file
139
tests/unit/quarantine-contract.test.mjs
Normal 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/);
|
||||
});
|
||||
17
vitest.quarantine.config.ts
Normal file
17
vitest.quarantine.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue