test(release): add executable missing-heading regression (#2685)

release.test.js only greps release.sh for one of the five
update_latest_release_heading call sites, and plugin-manifest.test.js
only checks the headings committed today. Neither executes the rewrite,
so a helper that silently no-ops on a missing heading would ship green.

Extract the embedded node program from release.sh and run it against
fixtures to pin the fail-closed contract: bump stable and prerelease
headings, leave the rest of the file untouched, and exit non-zero
without writing when no heading matches. Also pin all five call sites so
the docs/zh-CN/README.md regression cannot recur.

Runs standalone via node tests/scripts/release-heading.test.js.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
haelyra
2026-08-06 16:53:59 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent d791457aca
commit b3c679684b
+164
View File
@@ -0,0 +1,164 @@
/**
* Behavioral regression tests for release.sh's update_latest_release_heading.
*
* tests/scripts/release.test.js only greps release.sh for the call sites, and
* tests/plugin-manifest.test.js only checks the headings that are committed
* right now. Neither one executes the rewrite, so a regression that made the
* helper silently no-op on a missing heading would ship green. This file runs
* the real embedded program against fixtures and pins the fail-closed contract.
*
* Runs standalone: node tests/scripts/release-heading.test.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const repoRoot = path.join(__dirname, '..', '..');
const scriptPath = path.join(repoRoot, 'scripts', 'release.sh');
const source = fs.readFileSync(scriptPath, 'utf8');
/**
* Pull the node program out of the shell function so the test exercises the
* exact code release.sh ships rather than a copy that can drift from it.
*/
function extractHeadingProgram() {
const match = source.match(
/update_latest_release_heading\(\)\s*\{[\s\S]*?node -e '([\s\S]*?)'\s*"\$file"/
);
assert.ok(
match,
'release.sh should define update_latest_release_heading as a node -e program taking "$file"'
);
return match[1];
}
const headingProgram = extractHeadingProgram();
function runHeadingUpdate(contents, version) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-release-heading-'));
const file = path.join(dir, 'README.md');
try {
fs.writeFileSync(file, contents);
const result = spawnSync(process.execPath, ['-e', headingProgram, file, version], {
encoding: 'utf8',
});
return {
status: result.status,
stderr: result.stderr || '',
contents: fs.readFileSync(file, 'utf8'),
};
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
function runTests() {
console.log('\n=== Testing release.sh latest-release heading sync ===\n');
let passed = 0;
let failed = 0;
if (test('rewrites a stable release heading and leaves the rest of the file intact', () => {
const before = '# Title\n\n### v2.0.0 — Highlights\n\nBody text with v2.0.0 left alone.\n';
const result = runHeadingUpdate(before, '2.1.0');
assert.strictEqual(result.status, 0, `expected success, got stderr: ${result.stderr}`);
assert.ok(
result.contents.includes('### v2.1.0 — Highlights'),
'heading should be bumped to the new version and keep its trailing text'
);
assert.ok(
result.contents.includes('Body text with v2.0.0 left alone.'),
'only the heading line should be rewritten'
);
})) passed++; else failed++;
if (test('rewrites a prerelease heading', () => {
const result = runHeadingUpdate('### v2.0.0-rc.1 — Preview\n', '2.0.0-rc.2');
assert.strictEqual(result.status, 0, `expected success, got stderr: ${result.stderr}`);
assert.ok(
result.contents.includes('### v2.0.0-rc.2 — Preview'),
'prerelease headings should be bumped like stable ones'
);
})) passed++; else failed++;
if (test('fails closed and does not write when the release heading is missing', () => {
const before = '# Title\n\nNo release heading anywhere in this document.\n';
const result = runHeadingUpdate(before, '2.1.0');
assert.notStrictEqual(result.status, 0, 'a missing heading must be a hard failure');
assert.match(
result.stderr,
/could not update latest release heading/i,
'the failure should name the unmet expectation'
);
assert.strictEqual(
result.contents,
before,
'a failed heading update must leave the file byte-identical'
);
})) passed++; else failed++;
if (test('fails closed when the heading has no trailing description', () => {
// The regex requires a space plus trailing text, so a bare "### v2.0.0"
// is not a match. That must surface as an error, not a silent skip.
const before = '### v2.0.0\n';
const result = runHeadingUpdate(before, '2.1.0');
assert.notStrictEqual(result.status, 0, 'a bare heading is not a supported match');
assert.strictEqual(result.contents, before, 'nothing should be written on failure');
})) passed++; else failed++;
if (test('every localized README with a release heading is bumped by release.sh', () => {
// docs/zh-CN/README.md regressed once because it got a version-row bump
// without a heading bump. Pin all five call sites so a dropped one fails
// here instead of during a release.
const requiredFileVariables = [
'README_FILE',
'ROOT_ZH_CN_README_FILE',
'TR_README_FILE',
'PT_BR_README_FILE',
'ZH_CN_README_FILE',
];
for (const variable of requiredFileVariables) {
assert.ok(
source.includes(`update_latest_release_heading "$${variable}"`),
`release.sh should update the latest release heading for $${variable}`
);
}
})) passed++; else failed++;
if (test('heading updates run before the release commit is created', () => {
const lastHeadingUpdate = source.lastIndexOf('update_latest_release_heading "$');
const commitIndex = source.indexOf('git commit -m "chore: bump plugin version to $VERSION"');
assert.ok(lastHeadingUpdate >= 0, 'release.sh should update release headings');
assert.ok(commitIndex >= 0, 'release.sh should create the release commit');
assert.ok(
lastHeadingUpdate < commitIndex,
'heading updates should happen before the release commit'
);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();