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

205 lines
11 KiB
JavaScript

// tests/lib/fix-round-4-5b2-a-prose.test.mjs
// Prose pins for fix round 4.5b2, part A (plan
// .claude/projects/2026-10-31-guard-4-5b2-part-a, decisions 91, 107, 122 and
// 123): the fixes that are text a session reads. The tag in brackets is the
// part-A id from the round's plan; each tagged test fails on an assertion at
// e2d6bec, part 4's end point. The keep-behaviour checks sit inside the tagged
// test as "Control:" assertions.
// KIA the CHANGELOG lists part A's fixes (RXT, CNT, SFX, ESR, EVL, SIS, HDR)
// under ### Fixed, brings part 4's CSG, SLT and ESC bullets in line,
// lists the two MAJOR findings decision 122 keeps, the four MINOR and
// what part A leaves open under ### Known issues, and its door under
// ### Doors.
// HDR tests/hooks/bash-guard-false-stops.test.mjs names its red base
// 591f3c4. In the red clone this test passes at e2d6bec, because the
// working tree's tests/ is copied in; its red is shown under a named
// mutation that writes the old header back.
// No static import of a new export: the file must load at e2d6bec.
// 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. */
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. */
const flat = (s) => s.replace(/\s+/g, ' ').trim();
// --- KIA: the round's CHANGELOG entry ----------------------------------------------
// Part A stands in the same Unreleased section as parts 1 to 4. The section is
// found by its title, by the rule tests/lib/fix-round-4-5b-part-4-prose.test.mjs
// uses (copied, not imported): `## 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. */
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 bullets of one `### ` subsection of the round's CHANGELOG section, each flattened to one line. */
function changelogBullets(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}"`);
const out = [];
for (const l of lines.slice(s + 1)) {
if (/^### /.test(l)) break;
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);
}
// Part A's seven Fixed bullets, each with the ref it must name: the four review
// findings of part 4's review it fixes, the two operator decisions and the
// false test header. The ids are the review's, written out here.
const FIXED = [
['(RXT)', '5f1cc34c'], ['(CNT)', '97d1f547'], ['(SFX)', '27b0b46a'], ['(ESR)', 'ed40abdb'],
['(EVL)', 'decision 107'], ['(SIS)', 'decision 123'], ['(HDR)', '86488cd6'],
];
const FIXED_IDS = ['5f1cc34c', '97d1f547', '27b0b46a', 'ed40abdb'];
const NEW_TAGS = FIXED.map(([tag]) => tag);
// The two MAJOR findings decision 122 keeps open.
const KEPT_IDS = ['943f9841', 'a6735ef9'];
// The review's four MINOR findings, listed as open under decision 91.
const MINOR_IDS = ['03df3b0b', '78040bd7', '49873813', '79402171'];
const DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
const TWO_ESCAPED = 'two escaped `<` in a row';
test('[KIA] the CHANGELOG lists part A\'s seven fixes under ### Fixed, each with its ref', () => {
const fixed = changelogBullets('### Fixed');
// Red: one ### Fixed bullet per part-A tag, each naming its ref.
for (const [tag, ref] of FIXED) {
const hits = fixed.filter((b) => b.includes(tag) && b.includes(ref));
assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${tag} naming ${ref}; found ${hits.length}`);
}
// Control: no fixed finding is listed as open.
const known = changelogBullets('### Known issues');
for (const id of FIXED_IDS) {
assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`);
}
});
test('[KIA] the two MAJOR findings decision 122 keeps stand under ### Known issues, one bullet each', () => {
const known = changelogBullets('### Known issues');
// Red: each kept finding has exactly one Known-issues bullet, and that bullet names decision 122.
const bullets = [];
for (const id of KEPT_IDS) {
const hits = known.filter((b) => b.includes(id));
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
assert.ok(hits[0].includes('decision 122'), `the Known-issues bullet naming ${id} does not say decision 122: ${hits[0]}`);
bullets.push(hits[0]);
}
assert.notEqual(bullets[0], bullets[1], `${KEPT_IDS[0]} and ${KEPT_IDS[1]} must stand in two different Known-issues bullets`);
// Control: neither kept finding stands in a Fixed bullet marked with one of part A's tags.
const fixed = changelogBullets('### Fixed');
for (const id of KEPT_IDS) {
const hits = fixed.filter((b) => b.includes(id) && NEW_TAGS.some((tag) => b.includes(tag)));
assert.deepEqual(hits, [], `Control: ${id} must not stand in a ### Fixed bullet marked with one of ${NEW_TAGS.join(' ')}`);
}
});
test('[KIA] part 4\'s SLT bullet no longer promises a pass the guard refuses, and names the SIS exception', () => {
const slt = changelogBullets('### Fixed').filter((b) => b.includes('(SLT)'));
assert.equal(slt.length, 1, `### Fixed must hold exactly one bullet marked (SLT); found ${slt.length}`);
// Red: the old promise is gone.
assert.ok(!slt[0].includes('pass again'), `the (SLT) bullet must not hold "pass again": ${slt[0]}`);
// It names the gaps of part 4's review, what is refused as before, and the SIS exception.
for (const part of [...KEPT_IDS, 'refused as before', 'unless a backslash-newline splits']) {
assert.ok(slt[0].includes(part), `the (SLT) bullet must hold ${JSON.stringify(part)}: ${slt[0]}`);
}
assert.ok(!slt[0].includes('is text and passes, so'), `the (SLT) bullet must not hold "is text and passes, so": ${slt[0]}`);
});
test('[KIA] part 4\'s CSG and ESC bullets name the gaps part A fixes, ESC with the EVL exception', () => {
const fixed = changelogBullets('### Fixed');
// Red: the CSG bullet names the split-delimiter gap.
const csg = fixed.filter((b) => b.includes('(CSG)'));
assert.equal(csg.length, 1, `### Fixed must hold exactly one bullet marked (CSG); found ${csg.length}`);
assert.ok(csg[0].includes('27b0b46a'), `the (CSG) bullet must name review finding 27b0b46a: ${csg[0]}`);
// The ESC bullet names the whole-command limits gap and the EVL exception.
const esc = fixed.filter((b) => b.includes('(ESC)'));
assert.equal(esc.length, 1, `### Fixed must hold exactly one bullet marked (ESC); found ${esc.length}`);
for (const part of ['ed40abdb', 'still open before the run', TWO_ESCAPED]) {
assert.ok(esc[0].includes(part), `the (ESC) bullet must hold ${JSON.stringify(part)}: ${esc[0]}`);
}
for (const gone of ['no quote or heredoc operator before it', 'stands before the run']) {
assert.ok(!esc[0].includes(gone), `the (ESC) bullet must not hold ${JSON.stringify(gone)}: ${esc[0]}`);
}
});
test('[KIA] ### Known issues lists the four MINOR, decisions 107, 123 and 124, and part 2\'s bullet with its exceptions', () => {
const known = changelogBullets('### Known issues');
// Red: each MINOR finding has exactly one Known-issues bullet, and that bullet names decision 91.
for (const id of MINOR_IDS) {
const hits = known.filter((b) => b.includes(id));
assert.equal(hits.length, 1, `### Known issues must hold exactly one bullet naming ${id}; found ${hits.length}`);
assert.ok(hits[0].includes('decision 91'), `the Known-issues bullet naming ${id} does not say decision 91: ${hits[0]}`);
}
// What part A leaves open: one bullet per decision.
const d107 = known.filter((b) => b.includes('decision 107'));
assert.equal(d107.length, 1, `### Known issues must hold exactly one bullet naming decision 107; found ${d107.length}`);
const d124 = known.filter((b) => b.includes('decision 124'));
assert.equal(d124.length, 1, `### Known issues must hold exactly one bullet naming decision 124; found ${d124.length}`);
for (const part of ['setopt', 'emulate', 'set -o', 'shopt', 'alias']) {
assert.ok(d124[0].includes(part), `the decision 124 Known-issues bullet must hold ${JSON.stringify(part)}: ${d124[0]}`);
}
const d123 = known.filter((b) => b.includes('decision 123'));
assert.equal(d123.length, 1, `### Known issues must hold exactly one bullet naming decision 123; found ${d123.length}`);
// Part 2's bullet keeps its pair and names the exceptions part A widens.
const followed = known.filter((b) => b.includes(DELIM) && b.includes('stays blocked'));
assert.equal(followed.length, 1, `### Known issues must hold one bullet holding ${DELIM} and "stays blocked"; found ${followed.length}`);
for (const part of ['fix round 4.5b2 part A', TWO_ESCAPED]) {
assert.ok(followed[0].includes(part), `part 2's Known-issues bullet must hold ${JSON.stringify(part)}: ${followed[0]}`);
}
});
test('[KIA] ### Doors holds one line for decision 91', () => {
const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 91'));
assert.equal(doors.length, 1, `### Doors must hold exactly one bullet naming decision 91; found ${doors.length}`);
});
// --- HDR: the false-stops test header -----------------------------------------------
test('[HDR] tests/hooks/bash-guard-false-stops.test.mjs names its red base 591f3c4', () => {
const text = read('tests/hooks/bash-guard-false-stops.test.mjs');
assert.ok(!text.includes('loads at 12bca8b'), 'tests/hooks/bash-guard-false-stops.test.mjs must not hold "loads at 12bca8b"');
const hits = text.split('loads at 591f3c4').length - 1;
assert.equal(hits, 1, `tests/hooks/bash-guard-false-stops.test.mjs must hold "loads at 591f3c4" exactly once; found ${hits}`);
});