Files
fusion/scripts/check-quarantine-ledger.mjs
gsxdsm b85a6b8663 FN-7912: add quarantine-ledger deadline visibility check
Add a report-only script that surfaces flaky-test quarantine entries approaching their 14-day deletion clock, so maintainers can make deliberate rescue-or-expire decisions before entries silently expire.

- Add scripts/check-quarantine-ledger.mjs: reads scripts/lib/test-quarantine.json, computes days-remaining against the existing 14-day deletion clock (shared DELETION_CLOCK_DAYS from scripts/test-velocity-baseline.mjs), and buckets each entry as expired/near/healthy/unknown
- Support --warn-within=<days> (default 5) to tune the near-deadline window, --json for machine-readable output, and --strict as an opt-in local/CI gate (exits 1 on expired/near entries) while default mode stays exit-0 and non-blocking
- Wire pnpm check:quarantine-ledger script in package.json
- Add scripts/__tests__/check-quarantine-ledger.test.mjs covering deadline bucketing/sorting, empty/missing ledger handling, --strict behavior, and --json output shape
- Document the new command and its flags in docs/testing.md under the quarantine ledger/deletion ratchet section

Files changed:
 docs/testing.md                                    |  10 +
 package.json                                       |   1 +
 scripts/__tests__/check-quarantine-ledger.test.mjs | 159 ++++++++++++++++
 scripts/check-quarantine-ledger.mjs                | 202 +++++++++++++++++++++
 4 files changed, 372 insertions(+)

Fusion-Task-Id: FN-7912

Fusion-Task-Lineage: c08e2e09-473a-4ad0-8c27-43cbc3355168

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-12 21:21:10 -07:00

203 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
/*
FNXC:TestQuarantine 2026-07-12-00:00:
The flaky-test deletion ratchet had no visibility tool for entries approaching the `quarantinedAt + 14d` deletion deadline.
This report surfaces near-deadline quarantines so maintainers can make deliberate rescue-or-expire decisions while preserving the policy's report-only default; `--strict` is the opt-in enforcement path.
*/
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEFAULT_QUARANTINE_PATH, DELETION_CLOCK_DAYS } from "./test-velocity-baseline.mjs";
const MS_PER_DAY = 86_400_000;
const DEFAULT_WARN_WITHIN_DAYS = 5;
const REASON_MAX_LENGTH = 140;
const currentFilePath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(currentFilePath), "..");
function toDate(value) {
if (!value) return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function ageDays(quarantinedAt, now) {
const quarantinedAtDate = toDate(quarantinedAt);
if (!quarantinedAtDate) return null;
return Math.floor((now.getTime() - quarantinedAtDate.getTime()) / MS_PER_DAY);
}
function formatIsoDate(date) {
return date.toISOString().slice(0, 10);
}
function normalizeWarnWithinDays(value) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) {
throw new Error(`--warn-within must be a non-negative integer, got ${value}`);
}
return parsed;
}
function truncateReason(reason) {
const normalized = String(reason ?? "").replace(/\s+/g, " ").trim();
if (normalized.length <= REASON_MAX_LENGTH) return normalized;
return `${normalized.slice(0, REASON_MAX_LENGTH - 1)}…`;
}
function summarizeRows(rows) {
return rows.reduce(
(summary, row) => {
summary[row.status] += 1;
summary.total += 1;
return summary;
},
{ total: 0, expired: 0, near: 0, healthy: 0, unknown: 0 },
);
}
export function readLedger(ledgerPath) {
if (!existsSync(ledgerPath)) {
return { entries: [] };
}
const json = JSON.parse(readFileSync(ledgerPath, "utf8"));
if (json?.entries != null && !Array.isArray(json.entries)) {
throw new Error(`quarantine ledger ${ledgerPath} must have an "entries" array`);
}
return json ?? { entries: [] };
}
export function computeDeadlines(json, { now = new Date(), warnWithinDays = DEFAULT_WARN_WITHIN_DAYS } = {}) {
const entries = Array.isArray(json?.entries) ? json.entries : [];
const rows = entries.map((entry, index) => {
const quarantinedAtDate = toDate(entry?.quarantinedAt);
const age = ageDays(entry?.quarantinedAt, now);
const daysRemaining = age == null ? null : DELETION_CLOCK_DAYS - age;
const deadlineDate = quarantinedAtDate == null
? null
: new Date(quarantinedAtDate.getTime() + DELETION_CLOCK_DAYS * MS_PER_DAY);
let status = "unknown";
if (daysRemaining != null) {
if (daysRemaining <= 0) {
status = "expired";
} else if (daysRemaining <= warnWithinDays) {
status = "near";
} else {
status = "healthy";
}
}
return {
index,
file: entry?.file ?? "unknown",
reason: entry?.reason ?? "",
quarantinedAt: entry?.quarantinedAt ?? null,
ageDays: age,
daysRemaining,
deadline: deadlineDate == null ? null : formatIsoDate(deadlineDate),
status,
};
});
return rows.sort((a, b) => {
if (a.deadline == null && b.deadline == null) return a.index - b.index;
if (a.deadline == null) return 1;
if (b.deadline == null) return -1;
return a.deadline.localeCompare(b.deadline) || a.file.localeCompare(b.file) || a.index - b.index;
});
}
export function renderReport(rows, { warnWithinDays = DEFAULT_WARN_WITHIN_DAYS } = {}) {
const summary = summarizeRows(rows);
const lines = [
"Quarantine ledger deadline report",
`Deletion clock: quarantinedAt + ${DELETION_CLOCK_DAYS} days; near-deadline window: ${warnWithinDays} days`,
`Summary: total=${summary.total} expired=${summary.expired} near=${summary.near} healthy=${summary.healthy} unknown=${summary.unknown}`,
];
if (rows.length === 0) {
lines.push("Ledger is empty; nothing quarantined.");
return `${lines.join("\n")}\n`;
}
lines.push("Entries (soonest deadline first):");
for (const row of rows) {
const timing = row.status === "expired"
? `EXPIRED (${Math.abs(row.daysRemaining)} day${Math.abs(row.daysRemaining) === 1 ? "" : "s"} overdue)`
: row.daysRemaining == null
? "deadline unknown"
: `${row.daysRemaining} day${row.daysRemaining === 1 ? "" : "s"} remaining`;
const deadline = row.deadline == null ? "unknown" : row.deadline;
const reason = truncateReason(row.reason) || "no reason recorded";
lines.push(`- [${row.status}] ${row.file} — ${timing}; deadline=${deadline}; reason=${reason}`);
}
return `${lines.join("\n")}\n`;
}
function parseArgs(argv) {
const args = {
warnWithinDays: DEFAULT_WARN_WITHIN_DAYS,
json: false,
strict: false,
help: false,
};
for (const arg of argv) {
if (arg === "--json") {
args.json = true;
} else if (arg === "--strict") {
args.strict = true;
} else if (arg === "--help" || arg === "-h") {
args.help = true;
} else if (arg.startsWith("--warn-within=")) {
args.warnWithinDays = normalizeWarnWithinDays(arg.slice("--warn-within=".length));
} else {
throw new Error(`Unknown argument: ${arg}`);
}
}
return args;
}
export function main(argv = process.argv.slice(2), { rootDir = repoRoot, stdout = process.stdout, stderr = process.stderr, now = new Date(), ledgerPath = path.join(rootDir, DEFAULT_QUARANTINE_PATH) } = {}) {
let args;
try {
args = parseArgs(argv);
} catch (error) {
stderr.write(`${error.message}\n`);
return 1;
}
if (args.help) {
stdout.write("Usage: node scripts/check-quarantine-ledger.mjs [--warn-within=<days>] [--json] [--strict]\n");
return 0;
}
let ledger;
try {
ledger = readLedger(ledgerPath);
} catch (error) {
stderr.write(`Failed to read quarantine ledger: ${error.message}\n`);
return 1;
}
const rows = computeDeadlines(ledger, { now, warnWithinDays: args.warnWithinDays });
const summary = summarizeRows(rows);
if (args.json) {
stdout.write(`${JSON.stringify({ summary, rows }, null, 2)}\n`);
} else {
stdout.write(renderReport(rows, { warnWithinDays: args.warnWithinDays }));
}
return args.strict && (summary.expired > 0 || summary.near > 0) ? 1 : 0;
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
process.exitCode = main();
}