diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5a68a36..3a05600 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -65,6 +65,40 @@ command to bundle the TypeScript code into JavaScript: npm run bundle ``` +## Coverage Badge + +**CRITICAL: Before making ANY commit that includes code changes (changes to +files in the `src` directory), you MUST ensure the coverage badge is up to date. +This is mandatory for EVERY commit that includes code changes.** + +The coverage badge in `badges/coverage.svg` must accurately reflect the current +test coverage. The CI workflow includes a check that will fail if the coverage +badge is out of date, making the PR unmergeable. + +### Updating the Coverage Badge + +After making code changes and running tests: + +1. Run `npm run coverage` to update the coverage badge +1. Verify the badge is correct by running + `npx tsx script/check-coverage-badge.ts` +1. Commit the updated badge along with your code changes + +### Checking Coverage Badge Accuracy + +To verify the coverage badge is up to date: + +```bash +npx tsx script/check-coverage-badge.ts +``` + +This script compares the coverage percentage in `badges/coverage.svg` with the +actual coverage from `coverage/coverage-summary.json`. If they don't match, the +script will fail with an error message. + +The `npm run ci-test` script automatically runs this check, so PRs with +inaccurate coverage badges will fail CI and be unmergeable. + ## General Coding Guidelines - Follow standard TypeScript and JavaScript coding conventions and best diff --git a/badges/coverage.svg b/badges/coverage.svg index 1b3eb31..a2166f5 100644 --- a/badges/coverage.svg +++ b/badges/coverage.svg @@ -1 +1 @@ -Coverage: 100%Coverage100% \ No newline at end of file +Coverage: 92.42%Coverage92.42% \ No newline at end of file diff --git a/package.json b/package.json index a3689ef..fc7af0f 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ }, "scripts": { "bundle": "npm run format:write && npm run package", - "ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest", + "ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest && npx tsx script/check-coverage-badge.ts", "coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg", "format:write": "npx prettier --write .", "format:check": "npx prettier --check .", diff --git a/script/check-coverage-badge.ts b/script/check-coverage-badge.ts new file mode 100644 index 0000000..90a418e --- /dev/null +++ b/script/check-coverage-badge.ts @@ -0,0 +1,97 @@ +/** + * Utility script to verify that the coverage badge is up to date + * This compares the coverage percentage in the badge SVG with the actual coverage from Jest + */ + +import { readFileSync, existsSync } from 'fs' +import { dirname, resolve } from 'path' +import { fileURLToPath } from 'url' + +/** + * Extracts the coverage percentage from the coverage-summary.json file + * @returns The statement coverage percentage as a number + * @throws Error if coverage file cannot be found or parsed + */ +export function getCoverageFromSummary(): number { + const scriptDir = dirname(fileURLToPath(import.meta.url)) + const coveragePath = resolve(scriptDir, '../coverage/coverage-summary.json') + + if (!existsSync(coveragePath)) { + throw new Error( + 'Coverage summary not found. Run "npm run test" to generate coverage data.' + ) + } + + const coverageData = JSON.parse(readFileSync(coveragePath, 'utf-8')) + + if (!coverageData.total || !coverageData.total.statements) { + throw new Error('Invalid coverage summary format') + } + + return coverageData.total.statements.pct +} + +/** + * Extracts the coverage percentage from the coverage badge SVG file + * @returns The coverage percentage as a number + * @throws Error if badge file cannot be found or parsed + */ +export function getCoverageFromBadge(): number { + const scriptDir = dirname(fileURLToPath(import.meta.url)) + const badgePath = resolve(scriptDir, '../badges/coverage.svg') + + if (!existsSync(badgePath)) { + throw new Error( + 'Coverage badge not found. Run "npm run coverage" to generate the badge.' + ) + } + + const badgeContent = readFileSync(badgePath, 'utf-8') + + // Extract the coverage percentage from the badge SVG + // The format is: XX.XX% + const match = badgeContent.match(/(\d+\.?\d*)%<\/text>/) + + if (!match) { + throw new Error('Could not extract coverage percentage from badge') + } + + return parseFloat(match[1]) +} + +/** + * Validates that the coverage badge matches the actual coverage + * @throws Error if badge is out of date + */ +export function validateCoverageBadge(): void { + const actualCoverage = getCoverageFromSummary() + const badgeCoverage = getCoverageFromBadge() + + console.log(`Actual coverage: ${actualCoverage}%`) + console.log(`Badge coverage: ${badgeCoverage}%`) + + if (actualCoverage !== badgeCoverage) { + throw new Error( + `Coverage badge is out of date!\n` + + ` Actual coverage: ${actualCoverage}%\n` + + ` Badge coverage: ${badgeCoverage}%\n` + + ` Run "npm run coverage" to update the badge.` + ) + } + + console.log('✓ Coverage badge is up to date') +} + +// If run directly as a script, validate the badge +const isMainModule = + fileURLToPath(import.meta.url) === process.argv[1] || + fileURLToPath(import.meta.url) === resolve(process.argv[1]) + +if (isMainModule) { + try { + validateCoverageBadge() + } catch (error) { + console.error(error instanceof Error ? error.message : error) + process.exit(1) + } +}