// tests/lib/fix-round-4-5b-part-4-prose.test.mjs // Prose pins for fix round 4-5b, part 4 (plan // .claude/projects/2026-10-31-fix-round-4-5b-part-4, decision 90): the fixes // that are text a session reads. The tag in brackets is the part-4 id from the // round's brief; the tagged test fails on an assertion at 591f3c4, part 3's end // point. The keep-behaviour checks sit inside the tagged test as "Control:" // assertions. // KI4 the CHANGELOG lists part 4's fixes (CSG, SLT, ESC, CTB) under ### Fixed, // its door under ### Doors, the review's other findings under // ### Known issues, and the TOK headline no longer claims `never`. // NVR the [TOK] test title and part 2's end-to-end case name no longer claim // `never`. // No static import of a new export: the file must load at 591f3c4. // 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(); // --- KI4: the round's CHANGELOG entry ---------------------------------------------- // Part 4 stands in the same Unreleased section as parts 1 to 3. The section is // found by its title, by the rule tests/lib/fix-round-4-5b-part-3-prose.test.mjs // uses (copied, not imported): `## Unreleased — ` 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); } // The review's four MAJOR findings part 4 fixes, with their tags, and the // review's other findings it lists as open; the ids are the review's, written // out here. const FIXED = [['(CSG)', '3a34adbe'], ['(SLT)', 'ad8ad828'], ['(ESC)', '797784e0'], ['(CTB)', '1658fc64']]; const OPEN_IDS = ['4a880f70', 'fb643fb0', '73eed642', '6f09b931', '161545f6', 'fe3fe55b']; const NEVER = /\bnever\b/i; test('[KI4] the CHANGELOG lists part 4\'s four fixes, the Doors line and a TOK headline without never', () => { const fixed = changelogBullets('### Fixed'); // Red: one ### Fixed bullet per part-4 tag, each naming its review finding. for (const [tag, id] of FIXED) { const hits = fixed.filter((b) => b.includes(tag) && b.includes(id)); assert.equal(hits.length, 1, `### Fixed must hold exactly one bullet marked ${tag} naming ${id}; found ${hits.length}`); } // Red: a door for the round. const doors = changelogBullets('### Doors').filter((b) => b.includes('decision 90')); assert.equal(doors.length, 1, `### Doors must hold one bullet naming decision 90; found ${doors.length}`); // Control: exactly one Fixed bullet is marked (TOK). const tok = fixed.filter((b) => b.includes('(TOK)')); assert.equal(tok.length, 1, `Control: ### Fixed must hold exactly one bullet marked (TOK); found ${tok.length}`); // Red: its bold headline no longer claims `never`. const head = /^- \*\*(.+?)\*\*/.exec(tok[0]); assert.ok(head, `the ### Fixed bullet marked (TOK) must open with a bold headline: ${tok[0]}`); assert.ok(!NEVER.test(head[1]), `the (TOK) headline must not hold the word never: ${head[1]}`); // Control: no fixed finding is listed as open. const known = changelogBullets('### Known issues'); for (const [, id] of FIXED) { assert.deepEqual(known.filter((b) => b.includes(id)), [], `Control: the fixed finding ${id} must not stand under ### Known issues`); } }); test('[KI4] the CHANGELOG lists the review\'s other findings as known issues', () => { const known = changelogBullets('### Known issues'); // Red: each open finding has exactly one Known-issues bullet, and that bullet names decision 90. for (const id of OPEN_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 90'), `the Known-issues bullet naming ${id} does not say decision 90: ${hits[0]}`); } // Red: the tokenizer bullet names review finding 564d6dff. const tok = known.filter((b) => b.includes("'90s")); assert.equal(tok.length, 1, `### Known issues must hold exactly one bullet holding '90s; found ${tok.length}`); assert.ok(tok[0].includes('564d6dff'), `the tokenizer Known-issues bullet must name review finding 564d6dff: ${tok[0]}`); // Control: the tokenizer bullet keeps what part 3 pinned. for (const part of ['say "hi"', 'x --since ', '2026-10-27', '2026-10-28']) { assert.ok(tok[0].includes(part), `Control: the tokenizer Known-issues bullet must hold ${JSON.stringify(part)}: ${tok[0]}`); } for (const gone of ['"a "', 'accepted these']) { assert.ok(!tok[0].includes(gone), `Control: the tokenizer Known-issues bullet must not hold ${JSON.stringify(gone)}: ${tok[0]}`); } }); // --- NVR: the [TOK] test title and part 2's case name ------------------------------ test('[NVR] the [TOK] test title and part 2\'s case name no longer claim never', () => { // The one test title in the tokenizer tests that starts with [TOK], read as text. const titles = [...read('tests/lib/arg-parser-tokenize.test.mjs').matchAll(/test\('\[TOK\] ([^']*)'/g)].map((m) => m[1]); assert.equal(titles.length, 1, `tests/lib/arg-parser-tokenize.test.mjs must hold exactly one test title starting with [TOK]; found ${titles.length}`); assert.ok(!NEVER.test(titles[0]), `the [TOK] test title must not hold the word never: ${titles[0]}`); // The first name=" line after `case_tok() {` in part 2's end-to-end script. const lines = read('tests/e2e-fix-round-4-5b-part-2.sh').split('\n'); const starts = lines.map((l, i) => (l.trim() === 'case_tok() {' ? i : -1)).filter((i) => i !== -1); assert.equal(starts.length, 1, `tests/e2e-fix-round-4-5b-part-2.sh must define case_tok() exactly once; found ${starts.length}`); const names = lines.slice(starts[0] + 1).filter((l) => /^\s*name="/.test(l)); assert.ok(names.length >= 1, 'case_tok() in tests/e2e-fix-round-4-5b-part-2.sh must set name='); assert.ok(!NEVER.test(names[0]), `the name= line of case_tok() must not hold the word never: ${names[0]}`); });