Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/development/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,16 @@ inputs, actions, commands, and orchestration modes.
`validate:specifications` validates unique capability ownership, safe existing
repository-relative evidence paths, complete registration of top-level SDDs,
retrospective-baseline structure, and byte-for-byte freshness of the generated
`specs/CATALOG.md` view.
`specs/CATALOG.md` view. It also enforces the communication-program test ledger:
at least 128 semantic-publication cases and 136 localization cases, allocated to
disjoint test files. Parameterized declarations count once, and removing tests,
inflating an allocation, or assigning one file to both budgets fails the gate.

The ledger lives at
`src/architecture/github_communication_test_budget.json`. Its qualifying counts
are a conservative audit of new Jest `it`/`test` declarations since the two SDDs
landed at baseline `0bb83b75^`; a table-driven declaration is not multiplied by
its data rows.

Merge-queue contract tests cover ruleset-only targets, classic/ruleset union,
GitHub Actions workflow parsing on target and candidate refs, external exact
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"validate:docs-page": "node scripts/validate-docs-page-assets.cjs",
"validate:documentation": "node scripts/validate-documentation-contract.cjs",
"validate:workflows": "node scripts/validate-workflow-contract.cjs",
"generate:specifications": "node scripts/validate-specification-catalog.cjs --write",
"validate:specifications": "node scripts/validate-specification-catalog.cjs",
"generate:specifications": "node scripts/validate-specification-catalog.cjs --write && node scripts/validate-github-communication-test-budget.cjs",
"validate:specifications": "node scripts/validate-specification-catalog.cjs && node scripts/validate-github-communication-test-budget.cjs",
"lint:fix": "eslint src --fix",
"postinstall": "node scripts/install-git-hooks.cjs"
},
Expand Down
151 changes: 151 additions & 0 deletions scripts/validate-github-communication-test-budget.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env node

const fs = require('node:fs');
const path = require('node:path');
const { execFileSync } = require('node:child_process');

const DEFAULT_ROOT = path.resolve(__dirname, '..');
const LEDGER_PATH = 'src/architecture/github_communication_test_budget.json';
const EXPECTED_BUDGETS = Object.freeze({
'semantic-github-publication': Object.freeze({
requiredCases: 128,
spec: 'specs/semantic-github-publication-and-notification.md',
}),
'repository-locale-localization': Object.freeze({
requiredCases: 136,
spec: 'specs/repository-locale-and-localization.md',
}),
});
const TEST_DECLARATION = /^\s*(?:it|test)(?:\.each)?\s*\(/gmu;
const ADDED_TEST_DECLARATION = /^\+\s*(?:it|test)(?:\.each)?\s*\(/gmu;

function readLedger(root = DEFAULT_ROOT) {
return JSON.parse(fs.readFileSync(path.join(root, LEDGER_PATH), 'utf8'));
}

function validateCommunicationTestBudget(root, ledger) {
const errors = [];
if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) {
return ['communication test-budget ledger must be a JSON object.'];
}
if (ledger.version !== 1) errors.push('communication test-budget ledger version must be 1.');
if (ledger.baseline !== '0bb83b75^') errors.push('communication test-budget baseline must remain 0bb83b75^.');
if (!gitObjectExists(root, `${ledger.baseline}^{commit}`)) {
errors.push(`communication test-budget baseline is unavailable: ${String(ledger.baseline)}.`);
}
if (!Array.isArray(ledger.budgets)) return [...errors, 'communication test-budget ledger must declare budgets.'];
if (ledger.budgets.length !== Object.keys(EXPECTED_BUDGETS).length) {
errors.push(`communication test-budget ledger must contain exactly ${Object.keys(EXPECTED_BUDGETS).length} budgets.`);
}

const seenBudgets = new Set();
const seenFiles = new Set();
for (const budget of ledger.budgets) {
const expected = EXPECTED_BUDGETS[budget?.id];
if (!expected) {
errors.push(`unknown communication test budget: ${String(budget?.id)}.`);
continue;
}
if (seenBudgets.has(budget.id)) errors.push(`duplicate communication test budget: ${budget.id}.`);
seenBudgets.add(budget.id);
if (budget.spec !== expected.spec) errors.push(`${budget.id} must reference ${expected.spec}.`);
if (budget.requiredCases !== expected.requiredCases) {
errors.push(`${budget.id} must require exactly ${expected.requiredCases} cases.`);
}
const specPath = path.join(root, expected.spec);
if (!fs.existsSync(specPath)
|| !fs.readFileSync(specPath, 'utf8').includes(`at least **${expected.requiredCases} distinct`)) {
errors.push(`${budget.id} specification does not declare its ${expected.requiredCases}-case budget.`);
}
if (!Array.isArray(budget.files) || budget.files.length === 0) {
errors.push(`${budget.id} must allocate cases to at least one test file.`);
continue;
}
let allocatedCases = 0;
for (const entry of budget.files) {
if (!isTestPath(entry?.path)) {
errors.push(`${budget.id} has an invalid test path: ${String(entry?.path)}.`);
continue;
}
if (seenFiles.has(entry.path)) {
errors.push(`${entry.path} is allocated to more than one communication budget.`);
}
seenFiles.add(entry.path);
if (!Number.isSafeInteger(entry.qualifyingCases) || entry.qualifyingCases <= 0) {
errors.push(`${budget.id} has an invalid qualifying-case count for ${entry.path}.`);
continue;
}
allocatedCases += entry.qualifyingCases;
const testPath = path.join(root, entry.path);
if (!fs.existsSync(testPath) || !fs.statSync(testPath).isFile()) {
errors.push(`${budget.id} test file does not exist: ${entry.path}.`);
continue;
}
const qualifyingDeclarations = countQualifyingDeclarations(root, ledger.baseline, entry.path);
if (qualifyingDeclarations < entry.qualifyingCases) {
errors.push(`${entry.path} has ${qualifyingDeclarations} qualifying test declarations since ${ledger.baseline}, below its allocation of ${entry.qualifyingCases}.`);
}
}
if (budget.allocatedCases !== allocatedCases) {
errors.push(`${budget.id} allocatedCases is ${String(budget.allocatedCases)} but its file allocation totals ${allocatedCases}.`);
}
if (allocatedCases < expected.requiredCases) {
errors.push(`${budget.id} allocates ${allocatedCases} cases, below its required ${expected.requiredCases}.`);
}
}
for (const budgetId of Object.keys(EXPECTED_BUDGETS)) {
if (!seenBudgets.has(budgetId)) errors.push(`missing communication test budget: ${budgetId}.`);
}
return errors;
}

function countQualifyingDeclarations(root, baseline, testPath) {
if (!gitObjectExists(root, `${baseline}:${testPath}`)) {
return (fs.readFileSync(path.join(root, testPath), 'utf8').match(TEST_DECLARATION) ?? []).length;
}
const diff = execFileSync(
'git',
['diff', baseline, '--unified=0', '--', testPath],
{ cwd: root, encoding: 'utf8' },
);
return (diff.match(ADDED_TEST_DECLARATION) ?? []).length;
}

function gitObjectExists(root, object) {
try {
execFileSync('git', ['cat-file', '-e', object], { cwd: root, stdio: 'ignore' });
return true;
} catch {
return false;
}
}

function isTestPath(value) {
return typeof value === 'string'
&& /^src\/.+(?:__tests__\/.+\.test\.ts|\.test\.ts)$/u.test(value)
&& value === path.posix.normalize(value)
&& !value.split('/').includes('..');
}

function main() {
const ledger = readLedger(DEFAULT_ROOT);
const errors = validateCommunicationTestBudget(DEFAULT_ROOT, ledger);
if (errors.length > 0) {
console.error(errors.map(error => `- ${error}`).join('\n'));
process.exitCode = 1;
return;
}
const summary = ledger.budgets
.map(budget => `${budget.id}=${budget.allocatedCases}/${budget.requiredCases}`)
.join(', ');
console.log(`GitHub communication test budgets: PASS (${summary}; no file is double-counted)`);
}

if (require.main === module) main();

module.exports = {
LEDGER_PATH,
countQualifyingDeclarations,
readLedger,
validateCommunicationTestBudget,
};
Loading