Round 8 (0f400d3..29b4ac9, BLOCK: 1 BLOCKER, 12 MAJOR, 1 MINOR). Each fix
began with a test that failed on 29b4ac9 (13 tests, all red there); suite
2221 (2219/0/2).
- BLOCKER: a backticked check whose trailing comment holds a quote, $,
backtick or backslash is refused, not dropped. The comment was never
screened, while the executor hands the Verify span as written to red
proof, the test lock and the done line inside --cmd "...", where a $(...)
runs and a quote ends the argument. trekplan's text and the reviewer's
NOT RUN table say so.
- A span that is only a comment is not-a-command, not a placeholder. The
dropComment text says the allowlist still refuses a word with a # in it.
- The criteria runner, comparison-gate and end-state-gate use isMain: they
did nothing and exited 0 under a symlinked path. The test now refuses any
process.argv[1] comparison outside lib/util/is-main.mjs.
- Files: in a field with backticked paths, a plain word counts only before
the entry's description (a dash, or a colon after the path); a name with
no slash only first in its entry or after and/or/+/&. "mocks
fs.readFileSync", "runs on Node.js 22.x" or "no change to package.json"
no longer make a tests-only step look like it builds something.
Lowercase makefile, rakefile and justfile are build files. Measured on the
520 Files: fields in 48 plans of the marketplace and the test repo:
3 fields read differently (two notes no longer read as files, one
duplicate name dropped), tests-only changed in 0.
- The test lock compares --cmd without its trailing comment, and the test
lock and the done line read a stop with stoppedBy, the one reading.
red-proof drops its second reading, spawnError.
- release-check reads a relative --review in --repo or, when only that one
exists, from the current directory as 6.3.0 did; two different files are
exit 2, never guessed. The runbook says so.
- One trailing-json reader: a closing fence may be longer than three
backticks, in the block reader and the verdict gate alike, and the review
validator and the synthesis digest use it, so three backticks inside a
JSON string no longer hide a BLOCKER from the remediation-step check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
514 lines
22 KiB
JavaScript
514 lines
22 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 } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
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: { ...process.env, 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('release-check r3 — scope_sha_end is read as written: an all-digit sha keeps its leading zero', () => {
|
|
assert.equal(typeof mod.reviewedSha, 'function');
|
|
assert.equal(mod.reviewedSha('---\ntype: trekreview\nscope_sha_end: 0123456\n---\nbody\n'), '0123456');
|
|
assert.equal(mod.reviewedSha('---\nscope_sha_end: "00ab12c"\n---\n'), '00ab12c');
|
|
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(); }
|
|
});
|