// tests/scripts/release-check.test.mjs // P13 — the check before a release (wave-2 order 8). One command answers "may // this HEAD be released?": the suite is green at HEAD, the review verdict for // what is released is not BLOCK (K7) and covers it, and every delivery in the // release says whether it is a one-way or a two-way door. // 0 = ready, 1 = not ready (listed), 2 = could not run (never green). // // Every test builds a throwaway git repo shaped like a release: one commit with // the work, then a release commit that only bumps versions and the CHANGELOG. // The repo's own `npm test` is a one-liner that exits 0 or 1, so the suite // check runs for real without running Voyage's suite inside its own suite. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, existsSync, symlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { childEnv } from '../helpers/child-env.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const CHECK = join(ROOT, 'scripts', 'release-check.mjs'); const mod = existsSync(CHECK) ? await import('../../scripts/release-check.mjs') : {}; const git = (cwd, ...args) => { const r = spawnSync('git', args, { cwd, encoding: 'utf8' }); if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`); return r.stdout.trim(); }; function reviewText({ verdict = 'ALLOW', sha }) { const id = 'a'.repeat(40); const severity = verdict === 'BLOCK' ? 'BLOCKER' : 'MINOR'; const fmVerdict = verdict === null ? '' : `verdict: ${verdict}\n`; const json = JSON.stringify({ verdict: verdict ?? undefined, counts: { BLOCKER: severity === 'BLOCKER' ? 1 : 0, MAJOR: 0, MINOR: severity === 'MINOR' ? 1 : 0, SUGGESTION: 0 }, allow_blocked_by: [], findings: [{ id, severity, rule_key: 'MISSING_TEST', file: 'a.mjs', line: 1, title: 't', detail: 'd', recommended_action: 'Add a test.' }], }, null, 2); return `--- type: trekreview review_version: "1.1" task: "t" slug: s project_dir: .claude/projects/2026-10-08-s/ brief_path: .claude/projects/2026-10-08-s/brief.md scope_sha_end: ${sha} reviewed_files_count: 1 ${fmVerdict}findings: - ${id} --- # Review: t ## Executive Summary x ## Coverage | File | Treatment | Reason | |------|-----------|--------| | a.mjs | deep-review | default | ## Remediation Summary x \`\`\`json ${json} \`\`\` `; } const DOORS = `### Doors - two-way: the fixture change — revert it and release a new patch. `; /** * A repo with the work in one commit and a release commit for 1.0.1 on top. * Options switch one thing off at a time; the default is a ready release. */ function fixture({ suiteExit = 0, doors = DOORS, verdict = 'ALLOW', writeReview = true, reviewSha = 'work', afterRelease = null, dirty = false, version = '1.0.1', testScript = null, files = {}, attributes = null, sub = null, } = {}) { const base = mkdtempSync(join(tmpdir(), 'release-check-')); const repo = join(base, 'repo'); // sub: the plugin lives in a subdirectory of a larger repo (release review of 6.3.0, round 2). const dir = sub ? join(repo, sub) : repo; mkdirSync(join(dir, '.claude-plugin'), { recursive: true }); git(repo, 'init', '-q', '-b', 'main'); git(repo, 'config', 'user.email', 't@example.invalid'); git(repo, 'config', 'user.name', 't'); git(repo, 'config', 'commit.gpgsign', 'false'); const pkg = (v) => JSON.stringify({ name: 'fx', version: v, scripts: { test: testScript ?? `node -e "process.exit(${suiteExit})"` } }, null, 2) + '\n'; writeFileSync(join(dir, 'package.json'), pkg('1.0.0')); writeFileSync(join(dir, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'fx', version: '1.0.0' }) + '\n'); writeFileSync(join(dir, 'README.md'), '# fx\n\n![Version](https://img.shields.io/badge/version-1.0.0-blue)\n\nText.\n'); writeFileSync(join(dir, 'CHANGELOG.md'), '# Changelog\n\n## v1.0.0 — 2026-10-01 — first\n\n- first\n'); writeFileSync(join(dir, 'a.mjs'), 'export const a = 1;\n'); for (const [rel, text] of Object.entries(files)) writeFileSync(join(dir, rel), text); if (attributes) writeFileSync(join(repo, '.git', 'info', 'attributes'), attributes); git(dir, 'add', '-A'); git(dir, 'commit', '-q', '-m', 'feat: first'); writeFileSync(join(dir, 'a.mjs'), 'export const a = 2;\n'); git(dir, 'add', 'a.mjs'); git(dir, 'commit', '-q', '-m', 'fix: the work'); const workSha = git(dir, 'rev-parse', 'HEAD'); if (version !== null) { writeFileSync(join(dir, 'package.json'), pkg(version)); writeFileSync(join(dir, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'fx', version }) + '\n'); } else { writeFileSync(join(dir, '.claude-plugin', 'plugin.json'), JSON.stringify({ name: 'fx' }) + '\n'); writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'fx', scripts: { test: 'node -e "process.exit(0)"' } }) + '\n'); } writeFileSync(join(dir, 'README.md'), `# fx\n\n![Version](https://img.shields.io/badge/version-${version ?? '1.0.0'}-blue)\n\nText.\n`); writeFileSync(join(dir, 'CHANGELOG.md'), `# Changelog\n\n## v${version ?? '9.9.9'} — 2026-10-08 — fixture\n\n${doors ?? ''}\n### Fixed\n\n- the work\n\n## v1.0.0 — 2026-10-01 — first\n\n- first\n`); git(dir, 'add', '-A'); git(dir, 'commit', '-q', '-m', 'chore(release): fixture'); if (afterRelease) { for (const [rel, text] of Object.entries(afterRelease)) { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), text); } git(dir, 'add', '-A'); git(dir, 'commit', '-q', '-m', 'after the review'); } if (dirty) writeFileSync(join(dir, 'a.mjs'), 'export const a = 3;\n'); const review = join(base, 'review.md'); const sha = reviewSha === 'work' ? workSha : reviewSha; if (writeReview) writeFileSync(review, reviewText({ verdict, sha })); return { base, repo: dir, review, cleanup: () => rmSync(base, { recursive: true, force: true }) }; } function run(fx, extra = []) { return spawnSync(process.execPath, [CHECK, '--repo', fx.repo, '--review', fx.review, ...extra], { encoding: 'utf8' }); } test('release-check — the CLI exists', () => { assert.ok(existsSync(CHECK), 'scripts/release-check.mjs must exist (P13)'); }); test('release-check — everything in place → 0 (ready)', () => { const fx = fixture(); try { const r = run(fx); assert.equal(r.status, 0, r.stdout + r.stderr); assert.match(r.stdout, /READY/); } finally { fx.cleanup(); } }); test('release-check — a ready release prints the release approval command (K2)', () => { const fx = fixture(); try { const r = run(fx); assert.equal(r.status, 0, r.stdout + r.stderr); assert.match(r.stdout, /approval-log\.mjs append --kind release --subject CHANGELOG\.md --sha256-of CHANGELOG\.md/); } finally { fx.cleanup(); } }); test('release-check — review verdict BLOCK → 1', () => { const fx = fixture({ verdict: 'BLOCK' }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /BLOCK/); } finally { fx.cleanup(); } }); test('release-check — no review.md → 2 (never green)', () => { const fx = fixture({ writeReview: false }); try { const r = run(fx); assert.equal(r.status, 2, r.stdout + r.stderr); } finally { fx.cleanup(); } }); test('release-check — a review the verdict gate cannot decide (no verdict) → 2', () => { const fx = fixture({ verdict: null }); try { const r = run(fx); assert.equal(r.status, 2, r.stdout + r.stderr); } finally { fx.cleanup(); } }); test('release-check — no door type in the release section → 1', () => { const fx = fixture({ doors: null }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /door/i); } finally { fx.cleanup(); } }); test('release-check — a door line that is neither one-way nor two-way → 1', () => { const fx = fixture({ doors: '### Doors\n\n- two-way: fine\n- maybe: not a door type\n' }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /maybe/); } finally { fx.cleanup(); } }); test('release-check — an empty Doors section → 1', () => { const fx = fixture({ doors: '### Doors\n\n' }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /no door entries/i); } finally { fx.cleanup(); } }); test('release-check — the suite red at HEAD → 1', () => { const fx = fixture({ suiteExit: 1 }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /suite/i); } finally { fx.cleanup(); } }); test('release-check — uncommitted changes → 1 (the suite would not measure HEAD)', () => { const fx = fixture({ dirty: true }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /uncommitted/i); } finally { fx.cleanup(); } }); test('release-check — a code change after the reviewed commit → 1 (the review does not cover it)', () => { const fx = fixture({ afterRelease: { 'a.mjs': 'export const a = 99;\n' } }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /a\.mjs/); } finally { fx.cleanup(); } }); test('release-check — a README prose change after the reviewed commit → 1 (only the badge may move)', () => { const fx = fixture({ afterRelease: { 'README.md': '# fx\n\n![Version](https://img.shields.io/badge/version-1.0.1-blue)\n\nNew text.\n' } }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /README\.md/); } finally { fx.cleanup(); } }); test('release-check — a review of a commit that is not in this history → 1', () => { const fx = fixture({ reviewSha: 'f'.repeat(40) }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /not in the history of HEAD/); } finally { fx.cleanup(); } }); test('release-check — no version to release (no --version, no plugin.json version) → 2', () => { const fx = fixture({ version: null }); try { assert.equal(run(fx).status, 2); } finally { fx.cleanup(); } }); test('release-check — not a git repo → 2', () => { const dir = mkdtempSync(join(tmpdir(), 'release-check-nogit-')); try { const r = spawnSync(process.execPath, [CHECK, '--repo', dir, '--review', join(dir, 'review.md'), '--version', '1.0.0'], { encoding: 'utf8' }); assert.equal(r.status, 2, r.stdout + r.stderr); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('release-check — --json reports every check and the exit', () => { const fx = fixture({ verdict: 'BLOCK' }); try { const r = run(fx, ['--json']); assert.equal(r.status, 1); const out = JSON.parse(r.stdout); assert.equal(out.exit, 1); assert.equal(out.version, '1.0.1'); assert.deepEqual(out.checks.map((c) => c.id), ['tree', 'suite', 'review', 'coverage', 'door']); assert.equal(out.checks.find((c) => c.id === 'review').status, 'fail'); } finally { fx.cleanup(); } }); test('release-check — parseDoors reads one-way and two-way entries and names the bad ones', () => { assert.equal(typeof mod.parseDoors, 'function'); const section = '### Doors\n\n- one-way: plan 1.9 rejects plan 1.7 files\n (users regenerate their plans)\n- two-way: a new check\n- maybe: x\n\n### Added\n\n- one-way: not in Doors\n'; const d = mod.parseDoors(section); assert.deepEqual(d.entries.map((e) => e.door), ['one-way', 'two-way']); assert.deepEqual(d.bad, ['- maybe: x']); assert.equal(d.found, true); }); test('release-check — releaseSection matches the CHANGELOG heading dialects exactly', () => { assert.equal(typeof mod.releaseSection, 'function'); const text = '# C\n\n## v1.10.0 — x\n\nten\n\n## [1.1.0] - y\n\none\n\n## v1.1.0-pre\n\npre\n'; assert.match(mod.releaseSection(text, '1.1.0'), /one/); assert.doesNotMatch(mod.releaseSection(text, '1.1.0'), /ten|pre/); assert.match(mod.releaseSection(text, '1.10.0'), /ten/); assert.equal(mod.releaseSection(text, '2.0.0'), null); }); // ---- release review of 6.3.0 (2026-10-10): what counts as bookkeeping ---------- const AFTER_PKG = JSON.stringify({ name: 'fx', version: '1.0.1', scripts: { test: 'node -e "process.exit(0)" && echo changed' } }, null, 2) + '\n'; test('release-check — a change to package.json after the review other than its version → 1', () => { const fx = fixture({ afterRelease: { 'package.json': AFTER_PKG } }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /package\.json \(lines other than the version\)/); } finally { fx.cleanup(); } }); test('release-check — a change to plugin.json after the review other than its version → 1', () => { const fx = fixture({ afterRelease: { '.claude-plugin/plugin.json': JSON.stringify({ name: 'fx', version: '1.0.1', hooks: './x.json' }) + '\n' } }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /plugin\.json \(lines other than the version\)/); } finally { fx.cleanup(); } }); test('release-check — a README line that starts with "++ " is a change, not a diff header → 1', () => { const fx = fixture({ afterRelease: { 'README.md': '# fx\n\n![Version](https://img.shields.io/badge/version-1.0.1-blue)\n\nText.\n++ new claim\n' } }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /README\.md \(lines other than the version badge\)/); } finally { fx.cleanup(); } }); test('release-check — a README diff that cannot be read → 2 (never green)', () => { const fx = fixture({ attributes: 'README.md diff=broken\n' }); try { spawnSync('git', ['config', 'diff.broken.textconv', 'false'], { cwd: fx.repo }); const r = run(fx); assert.equal(r.status, 2, r.stdout + r.stderr); assert.match(r.stdout, /README\.md/); } finally { fx.cleanup(); } }); test('release-check — a red suite is red when the check itself runs under node --test', () => { const fx = fixture({ testScript: 'node --test red.test.mjs', files: { 'red.test.mjs': "import { test } from 'node:test';\nimport assert from 'node:assert';\ntest('red', () => assert.equal(1, 2));\n" }, }); try { const control = run(fx); assert.equal(control.status, 1, `the control: the suite is red: ${control.stdout}`); const r = spawnSync(process.execPath, [CHECK, '--repo', fx.repo, '--review', fx.review], { encoding: 'utf8', env: childEnv({ NODE_TEST_CONTEXT: 'child-v8' }) }); assert.equal(r.status, 1, `under a parent test runner: ${r.stdout}`); assert.match(r.stdout, /npm test exited/); } finally { fx.cleanup(); } }); // ---- round 2 of the release review (2026-10-10) -------------------------------- test('release-check — a plugin in a subdirectory of a larger repo is checked there → 0', () => { const fx = fixture({ sub: 'voyage' }); try { const r = run(fx); assert.equal(r.status, 0, r.stdout + r.stderr); } finally { fx.cleanup(); } }); test('release-check — no review: the suite is not run, exit 2 at once', () => { const fx = fixture({ writeReview: false, testScript: 'node -e "require(\'fs\').writeFileSync(\'suite-ran\', \'\')"' }); try { const r = run(fx); assert.equal(r.status, 2, r.stdout + r.stderr); assert.equal(existsSync(join(fx.repo, 'suite-ran')), false, 'the suite did not run'); assert.match(r.stdout, /suite\s+not run/); } finally { fx.cleanup(); } }); test('release-check — a flag with no value is exit 2, not 1', () => { const r = spawnSync(process.execPath, [CHECK, '--repo'], { encoding: 'utf8' }); assert.equal(r.status, 2, r.stdout + r.stderr); assert.match(r.stdout + r.stderr, /not measured|NOT MEASURED/i); }); // ==== round 3 of the release review of 6.3.0 (2026-10-10) ==================== test('release-check r3 — the review and the approval log committed after the review do not count as unreviewed → 0', () => { const fx = fixture({ afterRelease: { '.claude/projects/2026-10-08-s/review.md': 'the review, committed (artifact_store: git)\n', '.claude/voyage/approvals.jsonl': '{"kind":"release"}\n', } }); try { const r = run(fx); assert.equal(r.status, 0, r.stdout + r.stderr); } finally { fx.cleanup(); } }); test('[M11] sibling: release-check r3 — scope_sha_end is read as written: an all-digit 40-character sha keeps its leading zero, a 7-character id is refused', () => { assert.equal(typeof mod.reviewedSha, 'function'); const digits = '0123456789012345678901234567890123456789'; assert.equal(mod.reviewedSha(`---\ntype: trekreview\nscope_sha_end: ${digits}\n---\nbody\n`), digits); const quoted = '00ab12c'.padEnd(40, '0'); assert.equal(mod.reviewedSha(`---\nscope_sha_end: "${quoted}"\n---\n`), quoted); const sha256 = '00ab12c'.padEnd(64, 'f'); assert.equal(mod.reviewedSha(`---\nscope_sha_end: ${sha256}\n---\n`), sha256, 'a 64-hex (SHA-256) id is read'); assert.equal(mod.reviewedSha('---\ntype: trekreview\nscope_sha_end: 0123456\n---\nbody\n'), '', 'a 7-character id is refused'); assert.equal(mod.reviewedSha('---\nscope_sha_end: "00ab12c"\n---\n'), '', 'a quoted 7-character id is refused'); assert.equal(mod.reviewedSha('---\ntype: trekreview\n---\nscope_sha_end: 1\n'), '', 'only the frontmatter counts'); }); // ==== round 4 of the release review of 6.3.0 (2026-10-10) ==================== test('release-check r4 — scope_sha_end must be a sha: a ref name fails coverage → 1', () => { assert.equal(mod.reviewedSha('---\nscope_sha_end: HEAD\n---\n'), ''); assert.equal(mod.reviewedSha('---\nscope_sha_end: main\n---\n'), ''); const fx = fixture({ reviewSha: 'HEAD' }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /scope_sha_end/); } finally { fx.cleanup(); } }); test('release-check r4 — other text on the badge line is not release bookkeeping → 1', () => { const fx = fixture({ afterRelease: { 'README.md': '# fx\n\n![Version](https://img.shields.io/badge/version-1.0.1-blue) [install](https://example.invalid/i.sh)\n\nText.\n', } }); try { const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /README\.md/); } finally { fx.cleanup(); } }); // ==== round 5 of the release review of 6.3.0 (2026-10-11) ==================== test('release-check r5 — a test moved into an allowed place after the review is still a removal → 1', () => { const fx = fixture({ files: { 'guard.test.mjs': 'export const guarded = true;\n' } }); try { mkdirSync(join(fx.repo, '.claude', 'voyage'), { recursive: true }); git(fx.repo, 'mv', 'guard.test.mjs', '.claude/voyage/guard.test.mjs'); git(fx.repo, 'commit', '-q', '-m', 'move the test out of the suite'); const r = run(fx); assert.equal(r.status, 1, r.stdout + r.stderr); assert.match(r.stdout, /guard\.test\.mjs/); } finally { fx.cleanup(); } }); // ==== round 6 of the release review of 6.3.0 (2026-10-11) ==================== test('release-check r6 — a suite that never ends is stopped and read as not measured → 2', () => { const fx = fixture({ testScript: 'node -e "setTimeout(() => {}, 4000)"' }); try { const r = mod.releaseCheck({ repo: fx.repo, review: fx.review, suiteTimeoutMs: 1000 }); const suite = r.checks.find((c) => c.id === 'suite'); assert.equal(suite.status, 'error', JSON.stringify(suite)); assert.match(suite.detail, /timeout|killed/i); assert.equal(r.exit, 2); } finally { fx.cleanup(); } }); // ==== round 7 of the release review of 6.3.0 (2026-10-11), fixed in 6.3.1 ==== test('release-check r7 — a suite killed for too much output is not called a timeout → 2', () => { const fx = fixture({ testScript: "node -e \"process.stdout.write('x'.repeat(200000))\"" }); try { const r = mod.releaseCheck({ repo: fx.repo, review: fx.review, suiteMaxBuffer: 4096 }); const suite = r.checks.find((c) => c.id === 'suite'); assert.equal(suite.status, 'error', JSON.stringify(suite)); assert.doesNotMatch(suite.detail, /timeout/i, suite.detail); assert.match(suite.detail, /output/i, suite.detail); assert.equal(r.exit, 2); } finally { fx.cleanup(); } }); test('release-check r7 — a relative --review is read in --repo, like every other path → 0', () => { const fx = fixture(); try { mkdirSync(join(fx.repo, '.claude', 'projects', 'x'), { recursive: true }); writeFileSync(join(fx.repo, '.claude', 'projects', 'x', 'review.md'), readFileSync(fx.review, 'utf8')); writeFileSync(join(fx.repo, '.git', 'info', 'exclude'), '.claude/\n'); // Run from the directory above the repo, as `--repo ../voyage` from a sibling would. const r = spawnSync(process.execPath, [CHECK, '--repo', 'repo', '--review', '.claude/projects/x/review.md'], { cwd: fx.base, encoding: 'utf8' }); assert.doesNotMatch(r.stdout, /does not exist/, r.stdout); assert.equal(r.status, 0, r.stdout + r.stderr); } finally { fx.cleanup(); } }); // ==== round 8 of the release review of 6.3.0 (2026-10-11), fixed in 6.3.1 ==== test('release-check r8 — a relative --review written from the caller\'s directory, as 6.3.0 read it, still works → 0', () => { const fx = fixture(); try { // The review sits beside the repo (fx.base/review.md); run from fx.base. const r = spawnSync(process.execPath, [CHECK, '--repo', 'repo', '--review', 'review.md'], { cwd: fx.base, encoding: 'utf8' }); assert.doesNotMatch(r.stdout, /does not exist/, r.stdout); assert.equal(r.status, 0, r.stdout + r.stderr); } finally { fx.cleanup(); } }); test('release-check r8 — a relative --review that names two different files is not guessed → 2', () => { const fx = fixture(); try { writeFileSync(join(fx.repo, 'review.md'), readFileSync(fx.review, 'utf8')); writeFileSync(join(fx.repo, '.git', 'info', 'exclude'), 'review.md\n'); const r = spawnSync(process.execPath, [CHECK, '--repo', 'repo', '--review', 'review.md'], { cwd: fx.base, encoding: 'utf8' }); assert.equal(r.status, 2, r.stdout + r.stderr); assert.match(r.stdout, /two files/, r.stdout); } finally { fx.cleanup(); } }); // ==== round 9 of the release review of 6.3.0 (2026-10-11), fixed in 6.3.1 ==== test('release-check r9 / bdd46799 — one review.md reached through a symlinked --repo is one file, not two → 0', () => { const fx = fixture(); const link = join(fx.base, 'repo link'); try { writeFileSync(join(fx.repo, 'review.md'), readFileSync(fx.review, 'utf8')); writeFileSync(join(fx.repo, '.git', 'info', 'exclude'), 'review.md\n'); symlinkSync(fx.repo, link); // --repo through the symlink, run from the repo itself: process.cwd() is the // physical path, so the two path strings differ while the file is one. const r = spawnSync(process.execPath, [CHECK, '--repo', link, '--review', 'review.md'], { cwd: fx.repo, encoding: 'utf8' }); assert.doesNotMatch(r.stdout, /two files/, r.stdout); assert.equal(r.status, 0, r.stdout + r.stderr); } finally { rmSync(link, { force: true }); fx.cleanup(); } }); // ==== fix round 4.5b: only a full 40- or 64-hex commit id (M11 sibling) ====== test('[M11] sibling: the release check refuses a 64-hex branch name in a SHA-1 repository with the reason "(a full 40- or 64-hex commit id)"', () => { // In a SHA-1 repository a 64-hex name is no object id; git resolves it as a // ref name to wherever the branch points, so coverage would pass a review // of whatever the branch is moved to. const name = 'c0ffee'.padEnd(64, '0'); const fx = fixture({ reviewSha: name }); try { git(fx.repo, 'branch', name, 'HEAD~1'); const r = run(fx, ['--json']); assert.equal(r.status, 1, r.stdout + r.stderr); const coverage = JSON.parse(r.stdout).checks.find((c) => c.id === 'coverage'); assert.equal(coverage.status, 'fail', JSON.stringify(coverage)); assert.ok(coverage.detail.includes('(a full 40- or 64-hex commit id)'), `the reason names a full commit id: ${coverage.detail}`); } finally { fx.cleanup(); } });