149 lines
7.9 KiB
JavaScript
149 lines
7.9 KiB
JavaScript
// 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 — <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);
|
|
}
|
|
|
|
// 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]}`);
|
|
});
|