mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
* test: add critical-path coverage ratchets * fix: harden coverage ratchet enforcement * fix: fetch coverage comparison history * fix: close coverage policy downgrade paths * fix: verify changed critical statements * fix: ignore non-executable coverage changes * fix: distinguish mixed type-only coverage edits * fix: classify coverage changes by runtime output * fix: enforce runtime-significant whitespace changes * fix: classify multiline coverage changes as hunks * fix: group equal-line coverage rewrites
249 lines
9.2 KiB
JavaScript
249 lines
9.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import { readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { coverageExceptionErrors } from './coverage-policy-utils.mjs';
|
|
|
|
const EXPECTED_PACKAGES = ['server', 'web', 'cli', 'mcp', 'desktop'];
|
|
const METRICS = ['lines', 'branches', 'functions', 'statements'];
|
|
const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
|
|
const GLOB_PATTERN = /[*?[\]{}!]/;
|
|
|
|
export function compareCoveragePolicy(current, baseline) {
|
|
const errors = [];
|
|
for (const baselinePackage of baseline?.packages ?? []) {
|
|
const currentPackage = current.packages?.find(({ id }) => id === baselinePackage.id);
|
|
if (!currentPackage) {
|
|
errors.push(`coverage package ${baselinePackage.id} cannot be removed`);
|
|
continue;
|
|
}
|
|
for (const baselineBoundary of baselinePackage.boundaries ?? []) {
|
|
const fullId = `${baselinePackage.id}/${baselineBoundary.id}`;
|
|
const currentBoundary = currentPackage.boundaries?.find(
|
|
({ id }) => id === baselineBoundary.id
|
|
);
|
|
if (!currentBoundary) {
|
|
errors.push(`coverage boundary ${fullId} cannot be removed`);
|
|
continue;
|
|
}
|
|
for (const metric of METRICS) {
|
|
if (currentBoundary.thresholds?.[metric] < baselineBoundary.thresholds?.[metric]) {
|
|
errors.push(`${fullId} ${metric} threshold cannot decrease`);
|
|
}
|
|
}
|
|
for (const pattern of baselineBoundary.include ?? []) {
|
|
if (!currentBoundary.include?.includes(pattern)) {
|
|
errors.push(`${fullId} cannot remove governed include pattern ${pattern}`);
|
|
}
|
|
}
|
|
}
|
|
for (const field of ['testFiles', 'triggerPatterns']) {
|
|
for (const input of baselinePackage.runner?.[field] ?? []) {
|
|
if (!currentPackage.runner?.[field]?.includes(input)) {
|
|
errors.push(`${baselinePackage.id} cannot remove governed runner ${field} input ${input}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
export function validateCoveragePolicy({
|
|
policy,
|
|
baselinePolicy,
|
|
packageJson,
|
|
workflow,
|
|
configs,
|
|
fileExists = () => true,
|
|
today = new Date().toISOString().slice(0, 10),
|
|
}) {
|
|
const errors = [];
|
|
const packageIds = policy.packages?.map(({ id }) => id) ?? [];
|
|
const coverageJob = workflow.match(
|
|
/(?:^|\n) {2}critical-path-coverage:\n[\s\S]*?(?=\n {2}[a-zA-Z0-9_-]+:\n|$)/
|
|
)?.[0];
|
|
|
|
if (policy.schemaVersion !== 'critical-path-coverage/v1') {
|
|
errors.push('coverage policy must use critical-path-coverage/v1');
|
|
}
|
|
if (JSON.stringify(packageIds) !== JSON.stringify(EXPECTED_PACKAGES)) {
|
|
errors.push(`coverage policy packages must be ${EXPECTED_PACKAGES.join(', ')}`);
|
|
}
|
|
for (const metric of METRICS) {
|
|
if (policy.longTermTarget?.[metric] < 80) {
|
|
errors.push(`long-term ${metric} target must be at least 80%`);
|
|
}
|
|
}
|
|
|
|
const boundaryIds = new Set();
|
|
for (const packagePolicy of policy.packages ?? []) {
|
|
if (packagePolicy.report !== `coverage/${packagePolicy.id}/coverage-summary.json`) {
|
|
errors.push(`${packagePolicy.id} must use its canonical machine-readable coverage summary`);
|
|
}
|
|
for (const testFile of packagePolicy.runner?.testFiles ?? []) {
|
|
if (
|
|
path.posix.normalize(testFile) !== testFile ||
|
|
path.posix.isAbsolute(testFile) ||
|
|
GLOB_PATTERN.test(testFile) ||
|
|
!/\.test\.[cm]?[jt]sx?$/.test(testFile)
|
|
) {
|
|
errors.push(`${packagePolicy.id} coverage test files must be exact test paths`);
|
|
}
|
|
if (!fileExists(`${packagePolicy.id}/${testFile}`)) {
|
|
errors.push(`${packagePolicy.id} governed coverage test file does not exist: ${testFile}`);
|
|
}
|
|
}
|
|
for (const boundary of packagePolicy.boundaries ?? []) {
|
|
const fullId = `${packagePolicy.id}/${boundary.id}`;
|
|
if (boundaryIds.has(fullId)) errors.push(`duplicate boundary ${fullId}`);
|
|
boundaryIds.add(fullId);
|
|
if (
|
|
!boundary.description ||
|
|
!Array.isArray(boundary.include) ||
|
|
boundary.include.length === 0
|
|
) {
|
|
errors.push(`${fullId} requires a description and include patterns`);
|
|
}
|
|
for (const metric of METRICS) {
|
|
const threshold = boundary.thresholds?.[metric];
|
|
if (typeof threshold !== 'number' || threshold <= 0 || threshold > 100) {
|
|
errors.push(`${fullId} ${metric} threshold must be between 0 and 100`);
|
|
}
|
|
}
|
|
for (const exception of boundary.exceptions ?? []) {
|
|
errors.push(...coverageExceptionErrors(exception, fullId, today));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!packageJson.scripts?.['test:coverage']?.includes('scripts/run-coverage.mjs')) {
|
|
errors.push('package.json must expose the documented test:coverage command');
|
|
}
|
|
if (!packageJson.devDependencies?.['@vitest/coverage-v8']) {
|
|
errors.push('the root workspace must own @vitest/coverage-v8');
|
|
}
|
|
if (!workflow.includes('critical-path-coverage:')) {
|
|
errors.push('CI must define the Critical Path Coverage job');
|
|
}
|
|
if (!workflow.includes('needs.select-tests.outputs.coverage_packages')) {
|
|
errors.push('CI coverage must use the deterministic package selection output');
|
|
}
|
|
if (!workflow.includes('pnpm test:coverage --packages')) {
|
|
errors.push('CI must execute the documented coverage command');
|
|
}
|
|
if (!workflow.includes('COVERAGE_BASE_REF:')) {
|
|
errors.push('CI must compare coverage policy and changed critical files with the base commit');
|
|
}
|
|
if (!workflow.includes('needs.select-tests.outputs.base_sha')) {
|
|
errors.push('CI coverage must use the selector base SHA for every event type');
|
|
}
|
|
if (!coverageJob?.includes('fetch-depth: 0')) {
|
|
errors.push('CI coverage checkout must fetch the base commit used by coverage ratchets');
|
|
}
|
|
if (!workflow.includes('if: always() && needs.select-tests.outputs.coverage_packages')) {
|
|
errors.push('CI must preserve coverage artifacts after a failed ratchet');
|
|
}
|
|
if (!workflow.includes('actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a')) {
|
|
errors.push('CI must upload coverage with the immutable approved action');
|
|
}
|
|
|
|
for (const packageId of EXPECTED_PACKAGES) {
|
|
const config = configs[packageId] ?? '';
|
|
if (!config.includes("provider: 'v8'") || !config.includes('all: true')) {
|
|
errors.push(`${packageId} coverage must use V8 and include untested source`);
|
|
}
|
|
if (!config.includes("'src/**/*.d.ts'")) {
|
|
errors.push(`${packageId} coverage must exclude type declarations`);
|
|
}
|
|
for (const pattern of [
|
|
'src/**/__fixtures__/**',
|
|
'src/**/fixtures/**',
|
|
'src/**/generated/**',
|
|
'src/**/*.generated.*',
|
|
'src/**/types.ts',
|
|
'src/types/**/*.ts',
|
|
]) {
|
|
if (!config.includes(pattern)) errors.push(`${packageId} coverage must exclude ${pattern}`);
|
|
}
|
|
}
|
|
for (const packageId of ['server', 'web']) {
|
|
if (
|
|
(policy.packages.find(({ id }) => id === packageId)?.runner?.testFiles?.length ?? 0) === 0
|
|
) {
|
|
errors.push(`${packageId} critical coverage must use exact test files`);
|
|
}
|
|
}
|
|
|
|
errors.push(...compareCoveragePolicy(policy, baselinePolicy));
|
|
|
|
return errors;
|
|
}
|
|
|
|
function readBaselinePolicy(repoRoot) {
|
|
const baselineRef = process.env.COVERAGE_BASE_REF;
|
|
if (!baselineRef) return undefined;
|
|
if (!COMMIT_SHA_PATTERN.test(baselineRef)) {
|
|
throw new Error('COVERAGE_BASE_REF must be a hexadecimal commit ID.');
|
|
}
|
|
execFileSync('git', ['cat-file', '-e', `${baselineRef}^{commit}`], {
|
|
cwd: repoRoot,
|
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
});
|
|
let baselineText;
|
|
try {
|
|
baselineText = execFileSync(
|
|
'git',
|
|
['show', `${baselineRef}:docs/testing/critical-path-coverage.json`],
|
|
{
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
}
|
|
);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
return JSON.parse(baselineText);
|
|
}
|
|
|
|
async function main(repoRoot = process.cwd()) {
|
|
const [policy, packageJson, workflow, ...configTexts] = await Promise.all([
|
|
readFile(path.join(repoRoot, 'docs/testing/critical-path-coverage.json'), 'utf8').then(
|
|
JSON.parse
|
|
),
|
|
readFile(path.join(repoRoot, 'package.json'), 'utf8').then(JSON.parse),
|
|
readFile(path.join(repoRoot, '.github/workflows/ci.yml'), 'utf8'),
|
|
...EXPECTED_PACKAGES.map((packageId) =>
|
|
readFile(path.join(repoRoot, packageId, 'vitest.config.ts'), 'utf8')
|
|
),
|
|
]);
|
|
const configs = Object.fromEntries(
|
|
EXPECTED_PACKAGES.map((packageId, index) => [packageId, configTexts[index]])
|
|
);
|
|
const errors = validateCoveragePolicy({
|
|
policy,
|
|
baselinePolicy: readBaselinePolicy(repoRoot),
|
|
packageJson,
|
|
workflow,
|
|
configs,
|
|
fileExists: (relativePath) => existsSync(path.join(repoRoot, relativePath)),
|
|
});
|
|
|
|
if (errors.length > 0) {
|
|
throw new Error(`Coverage policy check failed:\n- ${errors.join('\n- ')}`);
|
|
}
|
|
console.log(
|
|
`Critical-path coverage policy covers ${EXPECTED_PACKAGES.length} packages and ${policy.packages.reduce((sum, packagePolicy) => sum + packagePolicy.boundaries.length, 0)} boundaries.`
|
|
);
|
|
}
|
|
|
|
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
if (isMain) {
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|