Files
fusion/scripts/test-feedback-baseline.mjs
gsxdsm f150a176b4 FN-6612: add test feedback-loop baseline reporting
Add a weekly baseline workflow for tracking test feedback-loop speed and flake pressure.

- Add a stdlib-only script to record gate and pnpm test timings alongside slowest test files and quarantine counts.
- Publish generated markdown and JSON baseline artifacts for #leads reporting.
- Document the weekly refresh process and add package scripts plus coverage for the baseline helper.

Files changed:
 docs/test-feedback-loop-baseline.md               |  48 ++++++
 docs/test-feedback-loop-baselines.json            | 122 ++++++++++++++
 docs/testing.md                                   |  12 ++
 package.json                                      |   1 +
 scripts/__tests__/test-feedback-baseline.test.mjs |  90 ++++++++++
 scripts/test-feedback-baseline.mjs                | 192 ++++++++++++++++++++++
 6 files changed, 465 insertions(+)

Fusion-Task-Id: FN-6612

Fusion-Task-Lineage: 72bdc070-50e3-4e6b-a07d-3b8aeb9c2e92
2026-06-17 19:25:06 -07:00

193 lines
9.5 KiB
JavaScript

#!/usr/bin/env node
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const currentFilePath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(currentFilePath), "..");
export const DEFAULT_TIMINGS_PATH = "scripts/test-timings.json";
export const DEFAULT_QUARANTINE_PATH = "scripts/lib/test-quarantine.json";
export const DEFAULT_BASELINES_PATH = "docs/test-feedback-loop-baselines.json";
export const DEFAULT_MARKDOWN_PATH = "docs/test-feedback-loop-baseline.md";
function readJson(relativePath, fallback = null, rootDir = repoRoot) {
const absolutePath = path.join(rootDir, relativePath);
if (!existsSync(absolutePath)) return fallback;
return JSON.parse(readFileSync(absolutePath, "utf8"));
}
function writeJson(relativePath, value, rootDir = repoRoot) {
const absolutePath = path.join(rootDir, relativePath);
mkdirSync(path.dirname(absolutePath), { recursive: true });
writeFileSync(absolutePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function writeText(relativePath, value, rootDir = repoRoot) {
const absolutePath = path.join(rootDir, relativePath);
mkdirSync(path.dirname(absolutePath), { recursive: true });
writeFileSync(absolutePath, value, "utf8");
}
function normalizeMs(value) {
if (value == null || value === "") return null;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error(`Expected a non-negative millisecond value, got ${value}`);
}
return Math.round(parsed);
}
function formatDuration(ms) {
if (ms == null) return "pending measurement";
const sign = ms < 0 ? "-" : "";
const absoluteMs = Math.abs(ms);
if (absoluteMs < 1000) return `${sign}${absoluteMs}ms`;
const seconds = absoluteMs / 1000;
if (seconds < 60) return `${sign}${seconds.toFixed(1)}s`;
const minutes = Math.floor(seconds / 60);
const remaining = Math.round(seconds - minutes * 60);
return `${sign}${minutes}m ${String(remaining).padStart(2, "0")}s`;
}
function isoWeek(date) {
const working = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
const day = working.getUTCDay() || 7;
working.setUTCDate(working.getUTCDate() + 4 - day);
const yearStart = new Date(Date.UTC(working.getUTCFullYear(), 0, 1));
const week = Math.ceil(((working - yearStart) / 86_400_000 + 1) / 7);
return `${working.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
}
export function collectSlowestFiles(timings, limit = 20) {
const rows = [];
for (const [packageName, packageTiming] of Object.entries(timings?.packages ?? {})) {
for (const [file, durationMs] of Object.entries(packageTiming?.files ?? {})) {
rows.push({ packageName, file, durationMs: Number(durationMs) || 0 });
}
}
rows.sort((a, b) => b.durationMs - a.durationMs || a.file.localeCompare(b.file));
return rows.slice(0, limit);
}
export function collectFlakeSummary(quarantine) {
const entries = Array.isArray(quarantine?.entries) ? quarantine.entries : [];
const uniqueFiles = [...new Set(entries.map((entry) => entry.file).filter(Boolean))].sort();
return {
flakeCount: entries.length,
uniqueQuarantinedFileCount: uniqueFiles.length,
quarantinedFiles: uniqueFiles,
};
}
export function createBaseline({ now = new Date(), gateWallTimeMs = null, pnpmTestWallTimeMs = null, timings, quarantine, notes = "" } = {}) {
const slowest20 = collectSlowestFiles(timings, 20);
const flakeSummary = collectFlakeSummary(quarantine);
return {
capturedAt: now.toISOString(),
cycle: isoWeek(now),
gateWallTimeMs: normalizeMs(gateWallTimeMs),
pnpmTestWallTimeMs: normalizeMs(pnpmTestWallTimeMs),
timingSnapshotCapturedAt: timings?.capturedAt ?? null,
slowest20,
...flakeSummary,
notes,
};
}
function trendDelta(latest, previous, field) {
if (!previous || latest?.[field] == null || previous?.[field] == null) return "n/a";
const delta = latest[field] - previous[field];
const sign = delta > 0 ? "+" : "";
return `${sign}${formatDuration(delta)}`;
}
export function renderMarkdown(baselines) {
const sorted = [...baselines].sort((a, b) => String(a.capturedAt).localeCompare(String(b.capturedAt)));
const latest = sorted.at(-1);
const previous = sorted.at(-2);
const slowRows = (latest?.slowest20 ?? [])
.map((row, index) => `| ${index + 1} | \`${row.file}\` | ${row.packageName} | ${formatDuration(row.durationMs)} |`)
.join("\n");
const trendRows = sorted
.map((row) => `| ${row.cycle} | ${row.capturedAt} | ${formatDuration(row.gateWallTimeMs)} | ${formatDuration(row.pnpmTestWallTimeMs)} | ${row.flakeCount ?? 0} | ${row.uniqueQuarantinedFileCount ?? 0} |`)
.join("\n");
return `# Test feedback-loop baseline\n\n> Publish this page's latest-cycle summary in #leads each week. The objective is signal-per-second: keep the merge gate thin, keep \`pnpm test\` flat or faster, and ratchet flaky/low-signal tests toward rescue or deletion.\n\n## Latest #leads summary\n\n- Cycle: **${latest?.cycle ?? "none"}** (${latest?.capturedAt ?? "not captured"})\n- Gate suite wall-time: **${formatDuration(latest?.gateWallTimeMs)}** (trend: ${trendDelta(latest, previous, "gateWallTimeMs")})\n- \`pnpm test\` wall-time: **${formatDuration(latest?.pnpmTestWallTimeMs)}** (trend: ${trendDelta(latest, previous, "pnpmTestWallTimeMs")})\n- Flake/quarantine count: **${latest?.flakeCount ?? 0}** ledger entr${(latest?.flakeCount ?? 0) === 1 ? "y" : "ies"} across **${latest?.uniqueQuarantinedFileCount ?? 0}** file${(latest?.uniqueQuarantinedFileCount ?? 0) === 1 ? "" : "s"}\n- Timing snapshot source: \`${DEFAULT_TIMINGS_PATH}\` captured at **${latest?.timingSnapshotCapturedAt ?? "unknown"}**\n\n## Slowest 20 test files\n\n| Rank | File | Package | Duration |\n|---:|---|---|---:|\n${slowRows || "| — | — | — | — |"}\n\n## Trend\n\n| Cycle | Captured at | Gate suite | \`pnpm test\` | Quarantine entries | Quarantined files |\n|---|---|---:|---:|---:|---:|\n${trendRows || "| — | — | — | — | — | — |"}\n\n## Operating rules\n\n- Record a new row weekly with \`node scripts/test-feedback-baseline.mjs --record --gate-ms <ms> --test-ms <ms>\` after running \`pnpm test:gate\` and \`pnpm test\`.\n- Use the slowest-file list as the candidate queue for FN-5048 rewrites or deletion-ratchet review; do not add coverage for its own sake.\n- Quarantined tests remain on the 14-day rescue-or-delete clock in \`scripts/lib/test-quarantine.json\`; deleting a low-signal expired test is a valid positive outcome.\n`;
}
function parseArgs(argv) {
const args = { record: false, printLeads: false, gateMs: null, testMs: null, notes: "" };
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--record") args.record = true;
else if (arg === "--print-leads") args.printLeads = true;
else if (arg === "--gate-ms") args.gateMs = argv[++index];
else if (arg === "--test-ms") args.testMs = argv[++index];
else if (arg === "--notes") args.notes = argv[++index] ?? "";
else if (arg === "--help" || arg === "-h") args.help = true;
else throw new Error(`Unknown argument: ${arg}`);
}
return args;
}
function renderLeadsSummary(baselines) {
const latest = [...baselines].sort((a, b) => String(a.capturedAt).localeCompare(String(b.capturedAt))).at(-1);
if (!latest) return "No test feedback-loop baseline has been recorded yet.";
const topFive = (latest.slowest20 ?? []).slice(0, 5).map((row, index) => `${index + 1}. ${row.file} (${formatDuration(row.durationMs)})`).join("; ");
return `Test feedback-loop ${latest.cycle}: gate ${formatDuration(latest.gateWallTimeMs)}, pnpm test ${formatDuration(latest.pnpmTestWallTimeMs)}, quarantine ledger ${latest.flakeCount} entries/${latest.uniqueQuarantinedFileCount} files. Slowest files: ${topFive || "none"}.`;
}
export async function main(argv = process.argv.slice(2), { rootDir = repoRoot, stdout = process.stdout, stderr = process.stderr } = {}) {
let args;
try {
args = parseArgs(argv);
} catch (err) {
stderr.write(`${err.message}\n`);
return 1;
}
if (args.help) {
stdout.write("Usage: node scripts/test-feedback-baseline.mjs [--record --gate-ms <ms> --test-ms <ms>] [--print-leads]\n");
return 0;
}
const store = readJson(DEFAULT_BASELINES_PATH, { baselines: [] }, rootDir);
const baselines = Array.isArray(store?.baselines) ? store.baselines : [];
if (args.record || baselines.length === 0) {
const timings = readJson(DEFAULT_TIMINGS_PATH, { packages: {} }, rootDir);
const quarantine = readJson(DEFAULT_QUARANTINE_PATH, { entries: [] }, rootDir);
baselines.push(createBaseline({
gateWallTimeMs: args.gateMs,
pnpmTestWallTimeMs: args.testMs,
timings,
quarantine,
notes: args.notes,
}));
writeJson(DEFAULT_BASELINES_PATH, { baselines }, rootDir);
}
const markdown = renderMarkdown(baselines);
writeText(DEFAULT_MARKDOWN_PATH, markdown, rootDir);
if (args.printLeads) {
stdout.write(`${renderLeadsSummary(baselines)}\n`);
} else {
stdout.write(`Updated ${DEFAULT_MARKDOWN_PATH}\n`);
}
return 0;
}
if (import.meta.url === `file://${process.argv[1]}`) {
/*
FNXC:TestFeedbackVelocity 2026-06-17-18:30:
The CEO mandate requires a weekly #leads-visible baseline for test signal-per-second, not more coverage. Keep this script stdlib-only so any engineer or scheduled job can refresh gate time, pnpm-test time, slowest files, and quarantine count without booting Fusion services.
*/
const exitCode = await main();
process.exitCode = exitCode;
}