P10's model-free part (wave 4, order A1, decision 63). The routing test now
checks every case in evals/ for from=, not only the skill-tagged ones (R1):
a case with no skill tag is not routed, only checked, and the report gives
both counts ("45 cases checked for from=, 37 routed"). The eight test-set
cases get from=proevesett-eval-suite (d311f3b) or
from=proevesett-intent-gate (3892a83), the commits that added them.
The release gate that reads the last test-set run is written into the
runbook as exempted with its reason (it waits for the fixed test set to run
again) and still says where it goes (scripts/release-check.mjs).
review-630's prototype-tag stub gets from=test: under R1 an unrouted case is
still checked for from=, and the stub tests that a prototype name is never
routed, which stands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
7.2 KiB
JavaScript
147 lines
7.2 KiB
JavaScript
// tests/lib/release-prose-pins.test.mjs
|
|
// Prose pins owned by the release process (wave-2 order 8, P13). Counted as
|
|
// doc pins by lib/util/test-census.mjs (PIN_FILE_RE matches `prose-pins.test.mjs`).
|
|
//
|
|
// The load-bearing pin: the rollback part of docs/release-runbook.md may only
|
|
// hold commands that were run in the release + rollback drill. The drill's
|
|
// rollback commands are recorded in tests/fixtures/release-drill/rollback-commands.txt
|
|
// (copied from the drill log, versions and commits written as the runbook's
|
|
// placeholders). A command written into § 3 that the drill never ran fails here.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parseDoors, releaseSection } from '../../scripts/release-check.mjs';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
|
|
|
const RUNBOOK = 'docs/release-runbook.md';
|
|
const FIXTURE = 'tests/fixtures/release-drill/rollback-commands.txt';
|
|
const APPROVAL = 'node lib/util/approval-log.mjs append --kind release --subject CHANGELOG.md --sha256-of CHANGELOG.md';
|
|
|
|
/** The lines from `## <heading>` to the next `## ` heading outside a fenced block. */
|
|
function part(md, heading) {
|
|
const lines = md.split('\n');
|
|
const start = lines.findIndex((l) => l === `## ${heading}`);
|
|
assert.ok(start !== -1, `${RUNBOOK} must have "## ${heading}"`);
|
|
let fenced = false;
|
|
let end = lines.length;
|
|
for (let i = start + 1; i < lines.length; i++) {
|
|
if (lines[i].startsWith('```')) fenced = !fenced;
|
|
if (!fenced && lines[i].startsWith('## ')) { end = i; break; }
|
|
}
|
|
return lines.slice(start, end).join('\n');
|
|
}
|
|
|
|
/** Every command line in the ```sh blocks of a markdown part. */
|
|
function shCommands(md) {
|
|
const out = [];
|
|
const re = /```sh\n([\s\S]*?)```/g;
|
|
let m;
|
|
while ((m = re.exec(md)) !== null) {
|
|
for (const line of m[1].split('\n')) {
|
|
const t = line.trim();
|
|
if (t && !t.startsWith('#')) out.push(t);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function drillCommands() {
|
|
return read(FIXTURE).split('\n')
|
|
.filter((l) => l.trim() && !l.startsWith('#'))
|
|
.map((l) => l.replace(/^\[(voyage|catalog)\] /, '').trim());
|
|
}
|
|
|
|
test('runbook: the three parts, in order — before, release, rollback', () => {
|
|
const md = read(RUNBOOK);
|
|
const at = ['## 1. Before the release', '## 2. The release', '## 3. Rollback'].map((h) => md.indexOf(`\n${h}`));
|
|
for (const i of at) assert.ok(i !== -1, `${RUNBOOK} must have each of the three parts`);
|
|
assert.ok(at[0] < at[1] && at[1] < at[2], 'the parts must come in the order before → release → rollback');
|
|
});
|
|
|
|
test('runbook: every command in the rollback part was run in the drill', () => {
|
|
const ran = new Set(drillCommands());
|
|
const written = shCommands(part(read(RUNBOOK), '3. Rollback'));
|
|
assert.ok(written.length >= 10, `the rollback part must carry the drilled commands (found ${written.length})`);
|
|
const unrun = written.filter((c) => !ran.has(c));
|
|
assert.deepEqual(unrun, [], `commands in § 3 that the drill never ran:\n${unrun.join('\n')}`);
|
|
});
|
|
|
|
test('drill fixture: both rollback paths were run — revert + ref back, and the forward patch', () => {
|
|
const ran = drillCommands();
|
|
for (const c of [
|
|
'git revert --no-edit <release-commit>',
|
|
'git revert --no-edit -m 1 <merge-commit>',
|
|
'node scripts/release-plugin.mjs voyage --version <previous-version> --write --commit --push',
|
|
'node scripts/release-plugin.mjs voyage --create-tag --write --commit --push',
|
|
'node scripts/check-versions.mjs',
|
|
]) assert.ok(ran.includes(c), `the drill fixture must record: ${c}`);
|
|
});
|
|
|
|
test('runbook § 1: the verdict gate (K7), the release check and the logged yes (K2)', () => {
|
|
const before = part(read(RUNBOOK), '1. Before the release');
|
|
assert.ok(before.includes('node lib/review/verdict-gate.mjs <review.md>'), '§ 1 must run the K7 verdict gate');
|
|
assert.ok(before.includes('node scripts/release-check.mjs --review <review.md>'), '§ 1 must run the release check');
|
|
assert.ok(before.includes(APPROVAL), '§ 1 must log the release approval with the K2 command the check prints');
|
|
assert.match(before, /npm test/, '§ 1 must require the suite on the merged tree');
|
|
});
|
|
|
|
test('runbook § 2: push last, then one release-plugin run, then the catalog check, then cleanup', () => {
|
|
const rel = shCommands(part(read(RUNBOOK), '2. The release'));
|
|
const order = [
|
|
'git push origin main',
|
|
'node scripts/release-plugin.mjs voyage --create-tag --write --commit --push',
|
|
'node scripts/check-versions.mjs',
|
|
].map((c) => rel.indexOf(c));
|
|
for (const i of order) assert.ok(i !== -1, '§ 2 must carry the push, the release run and the catalog check');
|
|
assert.ok(order[0] < order[1] && order[1] < order[2], '§ 2 order: git push, release-plugin, check-versions');
|
|
assert.match(part(read(RUNBOOK), '2. The release'), /Cleanup comes last/);
|
|
});
|
|
|
|
test('runbook: the door-type example parses as a release section with one-way and two-way lines', () => {
|
|
const md = part(read(RUNBOOK), 'Door types');
|
|
const block = md.match(/```markdown\n([\s\S]*?)```/);
|
|
assert.ok(block, 'the door types part must show the CHANGELOG form');
|
|
const section = releaseSection(block[1], '6.3.0');
|
|
assert.ok(section !== null, 'the example must be a release section');
|
|
const doors = parseDoors(section);
|
|
assert.deepEqual(doors.entries.map((e) => e.door).sort(), ['one-way', 'two-way']);
|
|
assert.deepEqual(doors.bad, []);
|
|
});
|
|
|
|
test('runbook: the release check exits as the script does — 0 ready, 1 not ready, 2 never green', () => {
|
|
const before = part(read(RUNBOOK), '1. Before the release').replace(/\s+/g, ' ');
|
|
assert.match(before, /0 = ready, 1 = not ready[^.]*, 2 = could not run — never green/);
|
|
});
|
|
|
|
test('runbook: the eval gate (P10) is exempted with its reason, and says where it goes', () => {
|
|
const p10 = part(read(RUNBOOK), 'Exempted: the eval gate (P10)').replace(/\s+/g, ' ');
|
|
assert.match(p10, /waits for the fixed test set to run again/, 'P10 must say why the gate is exempted');
|
|
assert.ok(p10.includes('scripts/release-check.mjs'), 'P10 must say which file the gate goes into');
|
|
});
|
|
|
|
test('runbook: public — no local paths', () => {
|
|
const md = read(RUNBOOK);
|
|
for (const bad of ['/Users/', '/private/', '/tmp/', '~/repos', 'scratchpad']) {
|
|
assert.ok(!md.includes(bad), `${RUNBOOK} must not carry a local path (${bad})`);
|
|
}
|
|
});
|
|
|
|
test('README: a section on releasing and rolling back that points to the runbook', () => {
|
|
const md = read('README.md');
|
|
const start = md.indexOf('\n## Releasing and rolling back');
|
|
assert.ok(start !== -1, 'README must have "## Releasing and rolling back"');
|
|
const sec = md.slice(start, md.indexOf('\n## ', start + 1));
|
|
assert.ok(sec.includes('docs/release-runbook.md'), 'the README section must link the runbook');
|
|
assert.ok(sec.includes('scripts/release-check.mjs'), 'the README section must name the release check');
|
|
});
|
|
|
|
test('CLAUDE.md: the pointer to the runbook and the release check', () => {
|
|
const md = read('CLAUDE.md');
|
|
assert.ok(md.includes('docs/release-runbook.md'), 'CLAUDE.md must point to the runbook');
|
|
assert.ok(md.includes('scripts/release-check.mjs'), 'CLAUDE.md must name the release check');
|
|
});
|