299 lines
17 KiB
JavaScript
299 lines
17 KiB
JavaScript
// tests/lib/fix-round-4-5b-prose.test.mjs
|
|
// Prose pins for fix round 4-5b (plan
|
|
// .claude/projects/2026-10-25-fix-round-4-5b-findings): the fixes that are
|
|
// text a session or a helper reads. The tags in brackets are 4.5b ids from the
|
|
// round's brief (for example `[M4]`, `[M11]`); each tagged test fails on an
|
|
// assertion at be0b631, the round's start point. A sibling's test is tagged
|
|
// `[<parent id>] sibling:`.
|
|
// M4 A git call the verdict gate cannot complete is reported as
|
|
// `review-not-measured:`, never as a stale review: Handover 6 names the
|
|
// prefix and lists review_version 1.1 as current, and /trekexecute's
|
|
// main-merge gate maps it to "not measured" with the merge locked.
|
|
// M11 Handover 6 states that only a full 40- or 64-hex reviewed commit is read.
|
|
// E22e The park tool parks every file new in the attempt's diff, untracked or
|
|
// staged but not in HEAD: its header and /trekexecute's resume paragraph
|
|
// no longer say "still untracked".
|
|
// M3 The "Back to planning" save line runs its git diff with --no-relative,
|
|
// so a session in a subdirectory with diff.relative on saves every file.
|
|
// M9 The review coordinator's Pass 2 text says what the lib does: a root
|
|
// escape or a negative line refutes a finding; any other line number
|
|
// alone never does, and a line past the end goes to `unverified`.
|
|
// M2 The four flag-table summaries of `--quick` (trekreview, orchestrator,
|
|
// command-modes, README) name the brief-conflict rule as the exception
|
|
// to the Pass 3 skip.
|
|
// K3 The orchestrator's 4-pass item 3 names the brief-conflict lowering as
|
|
// the exception to the catalogue reset.
|
|
// ARG /trekendsession and /trekcontinue Phase 0 and /trekreview Phase 1
|
|
// give $ARGUMENTS to the arg parser (`--stdin`) as the one body line of
|
|
// a quoted heredoc, never as shell words.
|
|
// M8 /trekcontinue and /trekendsession Phase 0 stop on the parser's exit 1
|
|
// with its `errors`, and stop when the parser prints no JSON.
|
|
// M10 /trekendsession Phase 2 reads the label from the parser's `tail`.
|
|
// No static import of a new export: the file must load at be0b631.
|
|
// 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();
|
|
|
|
/** The text from the line holding `start` up to (not including) the next line holding `stop`; both must exist. */
|
|
function between(rel, start, stop) {
|
|
const text = read(rel);
|
|
const s = text.indexOf(start);
|
|
assert.ok(s !== -1, `${rel} must hold "${start}"`);
|
|
const e = text.indexOf(stop, s + start.length);
|
|
assert.ok(e !== -1, `${rel} must hold "${stop}" after "${start}"`);
|
|
return text.slice(s, e);
|
|
}
|
|
|
|
/** Handover 6 of the handover contracts, up to Handover 7. */
|
|
const handover6 = () => between('docs/HANDOVER-CONTRACTS.md', '## Handover 6 ', '## Handover 7 ');
|
|
|
|
/** The one table row of Handover 6 whose first cell is `field`. */
|
|
function fieldRow(field) {
|
|
const rows = handover6().split('\n').filter((l) => l.startsWith(`| \`${field}\` |`));
|
|
assert.equal(rows.length, 1, `Handover 6 must hold exactly one \`${field}\` row; found ${rows.length}`);
|
|
return rows[0];
|
|
}
|
|
|
|
/** The verdict-gate paragraph of Handover 6 (the "Second consumer" one), flattened. */
|
|
function gateParagraph() {
|
|
const para = handover6().split('\n').filter((l) => l.startsWith('**Second consumer — the verdict gate'));
|
|
assert.equal(para.length, 1, `Handover 6 must hold one verdict-gate paragraph; found ${para.length}`);
|
|
return flat(para[0]);
|
|
}
|
|
|
|
/** Every sentence of `text` (flattened) that holds `needle`. */
|
|
function sentencesWith(text, needle) {
|
|
return flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/).filter((s) => s.includes(needle));
|
|
}
|
|
|
|
// --- M4 ----------------------------------------------------------------------------
|
|
|
|
test('[M4] Handover 6 names the review-not-measured reason', () => {
|
|
const hits = sentencesWith(gateParagraph(), '`review-not-measured:`');
|
|
assert.ok(hits.length > 0, 'the verdict-gate paragraph of Handover 6 does not name the `review-not-measured:` reason prefix');
|
|
assert.ok(hits.some((s) => /\bgit\b/.test(s) && /never green|not measured/.test(s)),
|
|
`no sentence ties \`review-not-measured:\` to a failed git call read as not measured: ${hits.join(' | ')}`);
|
|
});
|
|
|
|
test('[M4] Handover 6 lists review_version 1.1 as current', () => {
|
|
const row = fieldRow('review_version');
|
|
assert.match(row, /"1\.1"`? \(current\)/, `the review_version row does not list 1.1 as current: ${row}`);
|
|
assert.doesNotMatch(row, /"1\.0"`? \(current\)/, `the review_version row still lists 1.0 as current: ${row}`);
|
|
});
|
|
|
|
test('[M4] the main merge gate maps review-not-measured to not measured', () => {
|
|
const gate = between('commands/trekexecute.md', '**1. The verdict.**', '**2. Open:');
|
|
const hits = sentencesWith(gate, '`review-not-measured:`');
|
|
assert.ok(hits.length > 0, 'the main-merge gate (Phase 8, 1. The verdict) does not name the `review-not-measured:` reason prefix');
|
|
const ok = hits.find((s) => {
|
|
const rest = s.split('`review-not-measured:`').join('');
|
|
return /"not measured"|\bnot measured\b/.test(rest) && /never green/.test(rest) && /merge stays (locked|closed)/.test(rest);
|
|
});
|
|
assert.ok(ok, `no sentence says a \`review-not-measured:\` reason is reported as "not measured", never green, with the merge locked: ${hits.join(' | ')}`);
|
|
});
|
|
|
|
// --- M11 ---------------------------------------------------------------------------
|
|
|
|
test('[M11] Handover 6 states the full 40- or 64-hex reviewed commit', () => {
|
|
const row = fieldRow('scope_sha_end');
|
|
assert.match(row, /full 40- or 64-hex/, `the scope_sha_end row does not state the full 40- or 64-hex commit id: ${row}`);
|
|
assert.match(gateParagraph(), /`scope_sha_end`[^.]*full 40- or 64-hex/,
|
|
'the verdict-gate paragraph of Handover 6 does not state that only a full 40- or 64-hex scope_sha_end is read');
|
|
});
|
|
|
|
// --- E22e --------------------------------------------------------------------------
|
|
|
|
/** The sentences of `text` (flattened) that say what the park tool moves or parks. */
|
|
function parkSentences(text) {
|
|
return flat(text).split(/(?<=\.)\s+(?=[A-Z`"*(])/).filter((s) => /\b(moves|parks)\b/.test(s) && /\bfile\b/.test(s));
|
|
}
|
|
|
|
test('[E22e] the park header says untracked or staged but not in HEAD', () => {
|
|
const src = read('lib/util/park-attempt-files.mjs');
|
|
const header = src.slice(0, src.indexOf('\nimport '));
|
|
assert.ok(header.length > 0, 'lib/util/park-attempt-files.mjs has no header before its imports');
|
|
const comment = header.split('\n').map((l) => l.replace(/^\/\/ ?/, '')).join('\n');
|
|
assert.doesNotMatch(flat(comment), /still untracked/, 'the park header still says the tool parks only files "still untracked"');
|
|
const hits = parkSentences(comment);
|
|
assert.ok(hits.some((s) => /new in the attempt's diff/.test(s) && /untracked, or staged but not in HEAD/.test(s)),
|
|
`no header sentence says every file new in the attempt's diff is parked, untracked, or staged but not in HEAD: ${hits.join(' | ')}`);
|
|
});
|
|
|
|
test('[E22e] the execute command says the same', () => {
|
|
const para = between('commands/trekexecute.md', 'A step at `fix_rounds` 4 on `--resume` was re-planned', 'Then launch a fresh');
|
|
assert.doesNotMatch(flat(para), /still untracked/, 'the /trekexecute resume paragraph still says the tool moves only files "still untracked"');
|
|
const hits = parkSentences(para);
|
|
assert.ok(hits.some((s) => /new in the attempt's diff/.test(s) && /untracked, or staged but not in HEAD/.test(s)),
|
|
`no /trekexecute sentence says every file new in the attempt's diff is parked, untracked, or staged but not in HEAD: ${hits.join(' | ')}`);
|
|
});
|
|
|
|
// --- M3 ----------------------------------------------------------------------------
|
|
|
|
test('[M3] the back-to-planning save line carries --no-relative', () => {
|
|
const lines = read('commands/trekexecute.md').split('\n').filter((l) => l.startsWith('**Back to planning** (`fix_rounds = 4`): save `'));
|
|
assert.equal(lines.length, 1, `expected one "Back to planning" save line; found ${lines.length}`);
|
|
const m = lines[0].match(/save `([^`]+)`/);
|
|
assert.ok(m, `no backticked command on the save line: ${lines[0]}`);
|
|
assert.match(m[1], /^git (?:-c \S+ )*diff (?:--\S+ )*--no-relative (?:--\S+ )*\{start_sha\}$/,
|
|
`the save line's git diff does not carry --no-relative: ${m[1]}`);
|
|
});
|
|
|
|
// --- M9 ----------------------------------------------------------------------------
|
|
|
|
test('[M9] the coordinator text does not say a line number alone never refutes', () => {
|
|
const coord = flat(read('agents/review-coordinator.md'));
|
|
assert.doesNotMatch(coord, /\bA line number alone never refutes/,
|
|
'agents/review-coordinator.md still says a line number alone never refutes, next to a negative line that does');
|
|
const p2 = between('agents/review-coordinator.md', '### Pass 2 — HubSpot Judge filters', '### Pass 3 — ');
|
|
assert.ok(sentencesWith(p2, 'a citation that escapes the repo root, or a negative line, refutes the finding').length > 0,
|
|
'Pass 2 no longer says a root escape or a negative line refutes the finding');
|
|
const hits = sentencesWith(p2, 'line number alone never refutes');
|
|
assert.ok(hits.some((s) => /\b[Aa]ny other line number alone never refutes\b/.test(s)
|
|
&& /`unverified` \(`accuracy:line-out-of-range`\)/.test(s)),
|
|
`no Pass 2 sentence says any other line number alone never refutes and goes to \`unverified\`: ${hits.join(' | ')}`);
|
|
});
|
|
|
|
// --- M2 ----------------------------------------------------------------------------
|
|
|
|
/** The one line of `rel` that starts with `prefix` and holds `needle`. */
|
|
function oneLine(rel, prefix, needle) {
|
|
const hits = read(rel).split('\n').filter((l) => l.startsWith(prefix) && l.includes(needle));
|
|
assert.equal(hits.length, 1, `${rel} must hold exactly one line starting "${prefix}" with "${needle}"; found ${hits.length}`);
|
|
return hits[0];
|
|
}
|
|
|
|
const BRIEF_CONFLICT_SKIP = /the coordinator's reasonableness filter,? except the brief-conflict rule\b/;
|
|
|
|
test('[M2] each of the four flag-table summaries names the brief-conflict exception', () => {
|
|
const summaries = [
|
|
['commands/trekreview.md --quick row', oneLine('commands/trekreview.md', '| `--quick` |', 'reasonableness filter')],
|
|
['agents/review-orchestrator.md quick mode', flat(between('agents/review-orchestrator.md', '- `quick` — ', '- `validate` — '))],
|
|
['docs/command-modes.md --quick row', oneLine('docs/command-modes.md', '| `--quick` |', 'reasonableness filter')],
|
|
['README.md Quick row', oneLine('README.md', '| **Quick** | `/trekreview ', 'reasonableness filter')],
|
|
];
|
|
for (const [where, text] of summaries) {
|
|
assert.match(text, BRIEF_CONFLICT_SKIP, `${where} skips the reasonableness filter without the brief-conflict exception: ${text}`);
|
|
}
|
|
});
|
|
|
|
// --- K3 ----------------------------------------------------------------------------
|
|
|
|
test('[K3] orchestrator item 3 names the brief-conflict lowering as the exception to the reset', () => {
|
|
const item = flat(between('agents/review-orchestrator.md', '3. **Cloudflare reasonableness**', '4. **Compute verdict**'));
|
|
const at = item.indexOf('reset a severity that does not match its catalogue tier.');
|
|
assert.ok(at !== -1, `item 3 no longer resets a severity to its catalogue tier: ${item}`);
|
|
const next = sentencesWith(item.slice(at + 'reset a severity that does not match its catalogue tier.'.length), ' ')[0] ?? '';
|
|
assert.match(next, /\bexception to (the|this) reset\b/, `the sentence after the reset does not name an exception to it: ${next}`);
|
|
assert.match(next, /brief-conflict lowering/, `the exception to the reset is not the brief-conflict lowering: ${next}`);
|
|
assert.match(next, /MAJOR[^.]*\bMINOR\b/, `the exception does not say a MAJOR drops to MINOR: ${next}`);
|
|
assert.match(next, /`brief_lowered_from: MAJOR`/, `the exception does not name brief_lowered_from: MAJOR: ${next}`);
|
|
});
|
|
|
|
// --- ARG / M8 / M10 ----------------------------------------------------------------
|
|
|
|
const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
|
|
const ARGS_OPENER = `<<'${ARGS_DELIM}'`;
|
|
|
|
/** The text of a `## ` section of `rel` from the line starting with `heading` to the next `## ` line outside a fence. */
|
|
function phase(rel, heading) {
|
|
const lines = read(rel).split('\n');
|
|
const s = lines.findIndex((l) => l.startsWith(heading));
|
|
assert.ok(s !== -1, `${rel} must hold a heading starting "${heading}"`);
|
|
let fenced = false;
|
|
for (let i = s + 1; i < lines.length; i++) {
|
|
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
|
|
if (!fenced && lines[i].startsWith('## ')) return lines.slice(s, i).join('\n');
|
|
}
|
|
return lines.slice(s).join('\n');
|
|
}
|
|
|
|
// [file, command, the section that runs the parser]
|
|
const PARSER_PHASES = [
|
|
['commands/trekendsession.md', 'trekendsession', '## Phase 0 — `--help` handling'],
|
|
['commands/trekcontinue.md', 'trekcontinue', '## Phase 0 — `--help` handling'],
|
|
['commands/trekreview.md', 'trekreview', '## Phase 1 — Parse mode and validate input'],
|
|
];
|
|
|
|
/** The one runtime ```bash block of the section that runs the arg parser for `cmd`, as lines, with the parser line's index. */
|
|
function parserBlock(rel, cmd, heading) {
|
|
const text = phase(rel, heading);
|
|
const blocks = text.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0])
|
|
.filter((b) => b.includes(`lib/parsers/arg-parser.mjs --command ${cmd}`));
|
|
assert.equal(blocks.length, 1, `${rel} ${heading} must hold one \`\`\`bash block that runs the ${cmd} arg parser; found ${blocks.length}`);
|
|
const lines = blocks[0].split('\n');
|
|
const at = lines.findIndex((l) => l.includes(`lib/parsers/arg-parser.mjs --command ${cmd}`));
|
|
return { lines, at };
|
|
}
|
|
|
|
test('[ARG] each of the three parser lines reads the arguments from a quoted heredoc', () => {
|
|
for (const [rel, cmd, heading] of PARSER_PHASES) {
|
|
const { lines, at } = parserBlock(rel, cmd, heading);
|
|
const opener = lines[at].trim();
|
|
assert.ok(opener.endsWith(` ${ARGS_OPENER}`),
|
|
`${rel}: the parser line does not end with the quoted heredoc opener ${ARGS_OPENER}: ${opener}`);
|
|
assert.equal(lines[at + 1], '$ARGUMENTS',
|
|
`${rel}: the line inside the heredoc is not $ARGUMENTS alone: ${JSON.stringify(lines[at + 1])}`);
|
|
assert.match(opener, new RegExp(`^node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/parsers/arg-parser\\.mjs --command ${cmd} --stdin <<`),
|
|
`${rel}: the parser line does not read stdin (--command ${cmd} --stdin right after arg-parser.mjs): ${opener}`);
|
|
const filled = lines.filter((l) => l.trim() !== '');
|
|
assert.ok(lines[at + 2] === ARGS_DELIM && filled[filled.length - 1] === ARGS_DELIM,
|
|
`${rel}: the closing line ${ARGS_DELIM} does not follow $ARGUMENTS as the block's last non-empty line: ${JSON.stringify(lines)}`);
|
|
}
|
|
});
|
|
|
|
test('[ARG] no line outside the heredoc carries $ARGUMENTS in those blocks', () => {
|
|
for (const [rel, cmd, heading] of PARSER_PHASES) {
|
|
const { lines } = parserBlock(rel, cmd, heading);
|
|
const open = lines.findIndex((l) => l.trimEnd().endsWith(ARGS_OPENER));
|
|
assert.ok(open !== -1, `${rel}: the parser block opens no ${ARGS_OPENER} heredoc: ${JSON.stringify(lines)}`);
|
|
const close = lines.findIndex((l, i) => i > open && l.trim() === ARGS_DELIM);
|
|
assert.ok(close !== -1, `${rel}: the parser block's heredoc has no closing line ${ARGS_DELIM}`);
|
|
const outside = lines.filter((l, i) => (i <= open || i >= close) && l.includes('$ARGUMENTS'));
|
|
assert.deepEqual(outside, [], `${rel}: a line outside the heredoc carries $ARGUMENTS`);
|
|
}
|
|
});
|
|
|
|
const PHASE0 = '## Phase 0 — `--help` handling';
|
|
const STOP_COMMANDS = ['commands/trekcontinue.md', 'commands/trekendsession.md'];
|
|
|
|
test('[M8] /trekcontinue and /trekendsession Phase 0 stop on exit 1 with the parser\'s errors', () => {
|
|
for (const rel of STOP_COMMANDS) {
|
|
const hits = sentencesWith(phase(rel, PHASE0), 'exit 1');
|
|
assert.ok(hits.some((s) => /^On exit 1, print (its|the parser's) `errors` and stop\.$/.test(s)),
|
|
`${rel} Phase 0 does not say: on exit 1, print the parser's \`errors\` and stop: ${hits.join(' | ')}`);
|
|
}
|
|
});
|
|
|
|
test('[M8] /trekcontinue and /trekendsession Phase 0 stop when the parser prints no JSON', () => {
|
|
for (const rel of STOP_COMMANDS) {
|
|
const hits = sentencesWith(phase(rel, PHASE0), 'no JSON');
|
|
assert.ok(hits.some((s) => /\bprints no JSON\b/.test(s) && /\bthe parser did not run\b/.test(s) && /\band stop\.$/.test(s)),
|
|
`${rel} Phase 0 does not say: when the parser prints no JSON, say that it did not run and stop: ${hits.join(' | ')}`);
|
|
}
|
|
});
|
|
|
|
test('[M10] /trekendsession Phase 2 reads the label from tail', () => {
|
|
const p2 = flat(phase('commands/trekendsession.md', '## Phase 2 — Required args check'));
|
|
assert.doesNotMatch(p2, /`positional\[1\]`/, 'Phase 2 still reads <next-label> from positional[1], which splits a label at spaces');
|
|
assert.match(p2, /`<next-label>` is the parser's `tail`/, `Phase 2 does not read <next-label> from the parser's tail: ${p2}`);
|
|
});
|