538 lines
23 KiB
JavaScript
538 lines
23 KiB
JavaScript
// 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\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\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\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\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 [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(); }
|
|
});
|