voyage/tests/lib/fix-round-4-5b2-b-prose.test.mjs

116 lines
6.4 KiB
JavaScript

// tests/lib/fix-round-4-5b2-b-prose.test.mjs
// Prose pins for fix round 4.5b2, part B (plan
// .claude/projects/2026-10-31-review-gate-4-5b2): the parts of the round that
// are text a session reads. The tag in brackets is the part-B id from the
// round's plan; each tagged test fails on an assertion at the step's base.
// V12 review_version 1.2 is current: Handover 6's table and field row name
// it, no line of docs/HANDOVER-CONTRACTS.md still names 1.1 as current,
// the Versioning paragraph says what 1.1 -> 1.2 changed, MIGRATION.md
// holds the 1.2 section with its N-1 window, and every other place that
// states the current review_version says 1.2.
// The helpers below are copied, never imported from an earlier prose test.
// Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const readCache = new Map();
/** A repo file's text; the file must exist. Read from disk once per run. */
// Copied from tests/lib/fix-round-4-5b2-a-prose.test.mjs (read).
function read(rel) {
if (!readCache.has(rel)) {
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8'));
}
return readCache.get(rel);
}
/** Whitespace (line breaks too) collapsed to one space. */
// Copied from tests/lib/fix-round-4-5b2-a-prose.test.mjs (flat).
const flat = (s) => s.replace(/\s+/g, ' ').trim();
/** The text from `heading` up to the next `## ` heading; the heading must exist. */
// Copied from tests/lib/review-prose-pins.test.mjs (section).
function section(text, heading) {
const start = text.indexOf(heading);
assert.ok(start >= 0, `heading missing: ${heading}`);
const next = text.indexOf('\n## ', start + heading.length);
return text.slice(start, next === -1 ? undefined : next);
}
/** Every sentence of `text` (flattened). */
const sentences = (text) => flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/);
const CONTRACTS = 'docs/HANDOVER-CONTRACTS.md';
// --- V12: review_version 1.2 is current ------------------------------------------
test('[V12] docs/HANDOVER-CONTRACTS.md names 1.2 as current in the table and the row, and no line names 1.1 as current', () => {
const text = read(CONTRACTS);
const tableRows = text.split('\n').filter((l) => l.startsWith('| `review.md` | `review_version`'));
assert.equal(tableRows.length, 1, `the versioning table has one review.md row; found ${tableRows.length}`);
assert.match(tableRows[0], /\|\s*`1\.2`\s*\|\s*$/, `the versioning table's review.md row says 1.2 is current: ${tableRows[0]}`);
const h6 = section(text, '## Handover 6');
const fieldRows = h6.split('\n').filter((l) => l.startsWith('| `review_version` |'));
assert.equal(fieldRows.length, 1, `Handover 6 has one review_version row; found ${fieldRows.length}`);
assert.match(fieldRows[0], /"1\.2"`? \(current\)/, `the review_version row lists 1.2 as current: ${fieldRows[0]}`);
const stale = text.split('\n').filter((l) =>
/1\.1"?`?\s*\(current\)/.test(l)
|| /current is `?1\.1\b/.test(l)
|| (l.startsWith('| `review.md` | `review_version`') && /`1\.1`/.test(l)));
assert.deepEqual(stale, [], `no line of ${CONTRACTS} may name review_version 1.1 as current`);
});
test('[V12] the Handover 6 Versioning paragraph has the 1.1 -> 1.2 sentence', () => {
const h6 = section(read(CONTRACTS), '## Handover 6');
const paras = h6.split('\n').filter((l) => l.startsWith('**Versioning:**'));
assert.equal(paras.length, 1, `Handover 6 has one Versioning paragraph; found ${paras.length}`);
const para = flat(paras[0]);
assert.match(para, /current is `1\.2`/, `the Versioning paragraph says current is 1.2: ${para}`);
const hits = sentences(para).filter((s) => /1\.1/.test(s) && /1\.2/.test(s));
assert.ok(hits.length > 0, `no sentence of the Versioning paragraph names both 1.1 and 1.2: ${para}`);
assert.ok(hits.some((s) => /still validates/.test(s)),
`the 1.1 -> 1.2 sentence says a 1.1 review still validates: ${hits.join(' | ')}`);
assert.ok(hits.some((s) => /never opens/.test(s) && /unanswered MAJOR/.test(s) && /lock/.test(s)),
`the 1.1 -> 1.2 sentence says a 1.1 review never opens the lock with an unanswered MAJOR: ${hits.join(' | ')}`);
});
test('[V12] MIGRATION.md has the review_version 1.2 section with its N-1 window', () => {
const text = read('MIGRATION.md');
const heads = text.split('\n').filter((l) => /^## review_version 1\.2\b/.test(l));
assert.deepEqual(heads, ['## review_version 1.2 (from the next release)'],
'MIGRATION.md has exactly one "## review_version 1.2 (from the next release)" section');
const body = flat(section(text, heads[0]));
assert.match(body, /\bN-1\b/, `the 1.2 section names the N-1 window: ${body}`);
assert.match(body, /\b1\.1\b/, `the 1.2 section says what happens to a 1.1 review: ${body}`);
});
test('[V12] every current review_version statement names 1.2', () => {
const tpl = read('templates/trekreview-template.md');
assert.match(tpl.split('\n')[2], /^review_version: "1\.2"$/, 'the template\'s frontmatter says review_version "1.2"');
assert.ok(!tpl.includes('review_version: "1.1"'), 'the template no longer writes review_version "1.1"');
const coord = read('agents/review-coordinator.md');
assert.ok(coord.includes('review_version: "1.2"'), 'the coordinator writes review_version "1.2"');
assert.ok(!coord.includes('review_version: "1.1"'), 'the coordinator no longer writes review_version "1.1"');
const cmd = read('commands/trekreview.md');
assert.match(cmd, /\*\*The review is a gate \(review_version 1\.2,/, '/trekreview says the review is a gate at review_version 1.2');
assert.doesNotMatch(cmd, /\(review_version 1\.1,/, '/trekreview no longer says review_version 1.1 is current');
const claude = read('CLAUDE.md');
assert.ok(claude.includes('**Review is a gate (review_version 1.2).**'), 'CLAUDE.md says the review gate is review_version 1.2');
assert.ok(!claude.includes('(review_version 1.1)'), 'CLAUDE.md no longer names review_version 1.1');
const arch = read('docs/architecture.md');
assert.ok(arch.includes('review_version 1.2'), 'docs/architecture.md names review_version 1.2');
assert.ok(!arch.includes('review_version 1.1'), 'docs/architecture.md no longer names review_version 1.1');
});