193 lines
11 KiB
JavaScript
193 lines
11 KiB
JavaScript
// tests/lib/fix-round-4-5b2-c-prose.test.mjs
|
|
// Prose pins for fix round 4.5b2, part C (plan
|
|
// .claude/plans/trekplan-2026-11-03-review-gate-4-5b2-fixes.md, Step 7): the
|
|
// round's CHANGELOG entry. The tag in brackets is the part-C id from the
|
|
// round's plan; each tagged test fails on an assertion at the round's red base
|
|
// f1f5ab5, where the CHANGELOG holds no part C entry.
|
|
// PRC the CHANGELOG lists part C: under ### Fixed one group names each of
|
|
// the eight behaviour tags exactly once, one bullet per tag, each
|
|
// ending with the review finding it fixes; ### Doors holds the part C
|
|
// two-way line; ### Known issues holds one decision-137 bullet that
|
|
// names the guard's quoted-value hole with each of the eight commands
|
|
// measured in the plan's Context, "decision 137" and "its own round".
|
|
// No bullet of this round may hold a string an earlier prose test counts.
|
|
// 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)), '..', '..');
|
|
|
|
/** A repo file's text; the file must exist. */
|
|
function read(rel) {
|
|
assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
|
|
return readFileSync(join(ROOT, rel), 'utf8');
|
|
}
|
|
|
|
/** Whitespace (line breaks too) collapsed to one space. */
|
|
// Copied from tests/lib/fix-round-4-5b2-b-prose.test.mjs (flat).
|
|
const flat = (s) => s.replace(/\s+/g, ' ').trim();
|
|
|
|
// Part C stands in the same Unreleased section as the earlier parts. The
|
|
// section is found by its title: `## Unreleased — <title>` before the release,
|
|
// `## vX.Y.Z — <date> — <title>` after it, exactly once, up to the next `## `.
|
|
const CHANGELOG_TITLE = "The annotating skill, the run coupling and P10's model-free part";
|
|
const CHANGELOG_HEADING = new RegExp(`^## (?:Unreleased|v\\d+\\.\\d+\\.\\d+ — \\d{4}-\\d{2}-\\d{2}) — ${CHANGELOG_TITLE.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
|
|
|
/** The lines of the round's CHANGELOG section, heading excluded. */
|
|
// Copied from tests/lib/fix-round-4-5b2-b-prose.test.mjs (changelogSection).
|
|
function changelogSection() {
|
|
const lines = read('CHANGELOG.md').split('\n');
|
|
const at = [];
|
|
lines.forEach((l, i) => { if (CHANGELOG_HEADING.test(l.replace(/\s+$/, ''))) at.push(i); });
|
|
assert.equal(at.length, 1, `CHANGELOG.md must hold the "${CHANGELOG_TITLE}" section exactly once (found ${at.length})`);
|
|
let end = lines.length;
|
|
for (let i = at[0] + 1; i < lines.length; i++) if (/^## /.test(lines[i])) { end = i; break; }
|
|
return lines.slice(at[0] + 1, end);
|
|
}
|
|
|
|
/** The lines of one `### ` subsection of the round's CHANGELOG section, heading excluded. */
|
|
function subsection(heading) {
|
|
const lines = changelogSection();
|
|
const s = lines.findIndex((l) => l.replace(/\s+$/, '') === heading);
|
|
assert.ok(s !== -1, `the "${CHANGELOG_TITLE}" section of CHANGELOG.md must have "${heading}"`);
|
|
let end = lines.length;
|
|
for (let i = s + 1; i < lines.length; i++) if (/^### /.test(lines[i])) { end = i; break; }
|
|
return lines.slice(s + 1, end);
|
|
}
|
|
|
|
/** The bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
|
|
// Copied from tests/lib/fix-round-4-5b2-b-prose.test.mjs (changelogBullets).
|
|
function changelogBullets(heading) {
|
|
const out = [];
|
|
for (const l of subsection(heading)) {
|
|
if (/^- /.test(l)) out.push(l);
|
|
else if (out.length && l.trim() !== '' && /^\s/.test(l)) out[out.length - 1] += ` ${l}`;
|
|
else if (l.trim() !== '') out.push('');
|
|
}
|
|
return out.filter((b) => b !== '').map(flat);
|
|
}
|
|
|
|
const PRC_GROUP = '**Fix round 4.5b2, part C (decisions 135 and 137)**';
|
|
// The round's behaviour tags with the review finding each fixes (first 8 hex of
|
|
// the finding id, written out here from the plan's Context table). PRC itself,
|
|
// the CHANGELOG step's own tag, is in no count.
|
|
const PRC_FINDINGS = [
|
|
['DUP', 'e57259bf'],
|
|
['RSP', 'de594275'],
|
|
['NRL', 'fd5a6e18'],
|
|
['SYM', 'd81f3c45'],
|
|
['WHD', '561d6f11'],
|
|
['AMB', 'df9f9870'],
|
|
['CLU', 'b5ae9798'],
|
|
['MMG', '73e1d174'],
|
|
];
|
|
// Strings the earlier prose tests count in this section: none may stand in a
|
|
// bullet of this round.
|
|
const COUNTED = ['decision 88', 'decision 89', 'decision 90', 'decision 91', 'decision 107', 'decision 122',
|
|
'decision 123', 'decision 124', "'90s", "bash -c $'", "<<$'\\t'", 'VOYAGE_ARGS_EOF_7F3C9E', '6.5.0',
|
|
'`review_version` 1.2', 'ref the session can move', 'unchanged caller',
|
|
'(TOK)', '(HDG)', '(CKI)', '(DLC)', '(CQW)', '(PQO)', '(CSG)', '(SLT)', '(ESC)', '(CTB)',
|
|
'(RXT)', '(CNT)', '(SFX)', '(ESR)', '(EVL)', '(SIS)', '(HDR)',
|
|
'(ANS)', '(RLS)', '(RAP)', '(V12)', '(PRX)', '(RND)', '(CAP)', '(CRY)', '(FXL)', '(POL)', '(ENG)', '(CMD)',
|
|
'(DPR)', '(CAT)', '(MEV)', '(RMP)', '(PXB)', '(FN1)'];
|
|
// The eight commands the plan's Context measured as giving no question (decision
|
|
// 137): seven from critic rounds 1-2 and the risk assessor's Phase 8 form.
|
|
const MEASURED = [
|
|
'`git tag -m "Release one" v1.0`',
|
|
'`git -C "/tmp/a b" push origin main`',
|
|
'`git -c "x.y=a b" push origin main`',
|
|
'`echo "a b"#; git push origin main`',
|
|
"`git tag -m '' v1.0`",
|
|
'`git -C ~/"My Repos"/x merge --no-ff feat`',
|
|
'`npm --workspace "$WS" publish`',
|
|
'`git -C "<path with a space>" merge --no-ff feat`',
|
|
];
|
|
const DOOR = "- two-way: fix round 4.5b2 part C (decisions 135, 137) — a revert restores part B's behaviour.";
|
|
|
|
/** Part C's group under `### Fixed`: its intro text and its bullets, each flattened. */
|
|
function fixedGroup() {
|
|
const body = subsection('### Fixed');
|
|
const at = body.filter((l) => l.replace(/\s+$/, '') === PRC_GROUP).length;
|
|
assert.equal(at, 1, `### Fixed must hold the group ${PRC_GROUP} exactly once (found ${at})`);
|
|
const start = body.findIndex((l) => l.replace(/\s+$/, '') === PRC_GROUP);
|
|
const intro = [];
|
|
const bullets = [];
|
|
for (const l of body.slice(start + 1)) {
|
|
if (/^\*\*[^*].*\*\*\s*$/.test(l)) break;
|
|
if (/^- /.test(l)) bullets.push(l);
|
|
else if (bullets.length && l.trim() !== '' && /^\s/.test(l)) bullets[bullets.length - 1] += ` ${l}`;
|
|
else if (!bullets.length && l.trim() !== '') intro.push(l);
|
|
else if (bullets.length && l.trim() !== '') break;
|
|
}
|
|
return { intro: flat(intro.join(' ')), bullets: bullets.map(flat) };
|
|
}
|
|
|
|
/** How often `needle` stands in `text`. */
|
|
const occurrences = (text, needle) => text.split(needle).length - 1;
|
|
|
|
test('[PRC] the part C group names each of the eight behaviour tags exactly once', () => {
|
|
const { intro, bullets } = fixedGroup();
|
|
assert.ok(intro.includes('`f1f5ab5`'), `the group's intro names the red base f1f5ab5: ${intro}`);
|
|
assert.ok(intro.includes('`tests/fix-round-4-5b2-c-red.sh`'), `the group's intro names tests/fix-round-4-5b2-c-red.sh: ${intro}`);
|
|
assert.equal(PRC_FINDINGS.length, 8, 'Control: eight behaviour tags');
|
|
const text = [intro, ...bullets].join('\n');
|
|
for (const [tag] of PRC_FINDINGS) {
|
|
assert.equal(occurrences(text, `(${tag})`), 1, `the group must name (${tag}) exactly once; found ${occurrences(text, `(${tag})`)}`);
|
|
assert.equal(bullets.filter((b) => b.includes(`(${tag})`)).length, 1, `exactly one bullet of the group is marked (${tag})`);
|
|
}
|
|
assert.equal(bullets.length, PRC_FINDINGS.length, `the group holds one bullet per tag: found ${bullets.length} bullets`);
|
|
assert.equal(occurrences(text, '(PRC)'), 0, 'the group does not name PRC, the CHANGELOG step\'s own tag');
|
|
for (const b of bullets) {
|
|
assert.match(b, /^- \*\*.+?\*\* \([A-Z0-9]{3}\)\./, `each bullet opens with a bold headline and its tag: ${b}`);
|
|
for (const s of COUNTED) assert.ok(!b.includes(s), `no bullet of the group may hold ${JSON.stringify(s)}: ${b}`);
|
|
}
|
|
});
|
|
|
|
test('[PRC] each part C bullet names its review finding', () => {
|
|
const { bullets } = fixedGroup();
|
|
for (const [tag, id] of PRC_FINDINGS) {
|
|
const hits = bullets.filter((b) => b.includes(`(${tag})`));
|
|
assert.equal(hits.length, 1, `exactly one bullet of the group is marked (${tag}); found ${hits.length}`);
|
|
assert.ok(hits[0].endsWith(`It names review finding \`${id}\`.`),
|
|
`the (${tag}) bullet must end "It names review finding \`${id}\`.": ${hits[0]}`);
|
|
assert.equal(bullets.filter((b) => b.includes(id)).length, 1, `review finding ${id} stands in exactly one bullet of the group`);
|
|
}
|
|
// The CLU bullet says its quoted -m half stays "no run evidence" (decision 137).
|
|
const clu = bullets.filter((b) => b.includes('(CLU)'))[0];
|
|
assert.ok(clu.includes('`-m`'), `the (CLU) bullet names the quoted \`-m\` value: ${clu}`);
|
|
assert.ok(clu.includes('no run evidence'), `the (CLU) bullet says the quoted -m half stays "no run evidence": ${clu}`);
|
|
assert.ok(clu.includes('decision 137'), `the (CLU) bullet names decision 137: ${clu}`);
|
|
// Control: no fixed finding is listed as open.
|
|
const known = changelogBullets('### Known issues');
|
|
for (const [, id] of PRC_FINDINGS) {
|
|
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
|
|
}
|
|
});
|
|
|
|
test('[PRC] the Doors section has the part C two-way line', () => {
|
|
const doors = changelogBullets('### Doors');
|
|
const hits = doors.filter((b) => b.startsWith('- two-way: fix round 4.5b2 part C'));
|
|
assert.deepEqual(hits, [DOOR], `### Doors must hold exactly the one part C line ${DOOR}`);
|
|
for (const s of COUNTED) assert.ok(!hits[0].includes(s), `the door must not hold ${JSON.stringify(s)}, a string an earlier prose test counts`);
|
|
// Control: every door bullet of the section is a door line.
|
|
for (const b of doors) assert.match(b, /^- (one-way|two-way): \S/, `Control: every ### Doors bullet is a door line: ${b}`);
|
|
});
|
|
|
|
test('[PRC] the known issues hold one decision-137 guard bullet with each of the eight measured commands, "decision 137" and "its own round"', () => {
|
|
const known = changelogBullets('### Known issues');
|
|
const hits = known.filter((b) => b.includes('(part C, decision 137)'));
|
|
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet marked (part C, decision 137); found ${hits.length}`);
|
|
const b = hits[0];
|
|
assert.equal(MEASURED.length, 8, 'Control: eight measured commands');
|
|
for (const cmd of MEASURED) {
|
|
assert.ok(b.includes(cmd), `the decision-137 bullet must name the measured command ${cmd}: ${b}`);
|
|
}
|
|
assert.ok(b.includes('decision 137'), `the decision-137 bullet must hold "decision 137": ${b}`);
|
|
assert.ok(b.includes('its own round'), `the decision-137 bullet must hold "its own round": ${b}`);
|
|
for (const s of COUNTED) assert.ok(!b.includes(s), `the decision-137 bullet must not hold ${JSON.stringify(s)}: ${b}`);
|
|
});
|