voyage/tests/lib/pre-existing-bound.test.mjs
2026-11-03 11:23:21 +01:00

379 lines
21 KiB
JavaScript

// tests/lib/pre-existing-bound.test.mjs
// [PXB] fix round 4.5b2 part B, Step 14: nothing is pre-existing unless
// scope_sha_start is bound to the fork point from main (`main`, else
// `master`). Exactly two starts are bound: the fork point itself, and a later
// start reached through process files only (the approval log
// `.claude/voyage/approvals.jsonl` and the top-level `.claude/projects/`). A
// forged narrower start, an older start, a start reached through a source
// change, and a range that ends on main itself mark nothing pre-existing, in
// finalize and at the gate. A finding on the approval log or on a
// `.claude/projects/` file is never pre-existing.
// The process-files and --no-ff cases expect a mark kept and may already pass
// before the binding; the file as a whole is red there.
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { computeFindingIds } from '../../lib/parsers/finding-id.mjs';
import { childEnv } from '../helpers/child-env.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const GATE = join(ROOT, 'lib', 'review', 'verdict-gate.mjs');
const PRE = join(ROOT, 'lib', 'review', 'pre-existing.mjs');
const FINALIZE = join(ROOT, 'lib', 'review', 'review-finalize.mjs');
const REVIEW_REL = '.claude/projects/2026-11-02-p/review.md';
const LOG_REL = '.claude/voyage/approvals.jsonl';
// Loaded so that each new assertion fails on its own while a module or export is absent.
const preMod = existsSync(PRE) ? await import(pathToFileURL(PRE).href) : {};
const gateMod = existsSync(GATE) ? await import(pathToFileURL(GATE).href) : {};
const finalizeMod = existsSync(FINALIZE) ? await import(pathToFileURL(FINALIZE).href) : {};
const made = [];
after(() => { for (const b of made) rmSync(b, { recursive: true, force: true }); });
function g(cwd, ...args) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', env: childEnv() });
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
}
function putIn(dir, rel, text) {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), text);
}
const parse = (r) => { try { return JSON.parse(r.stdout); } catch { return {}; } };
// Ten lines `export const <tag><n> = <n>;`; `changes` maps a line number to new text, or to null to remove it.
const ten = (tag, changes = {}) => Array.from({ length: 10 }, (_, i) => (i + 1 in changes ? changes[i + 1] : `export const ${tag}${i + 1} = ${i + 1};`))
.filter((l) => l !== null).join('\n') + '\n';
// Copied from prxRepo in tests/lib/pre-existing.test.mjs:70-115, without its .claude/projects/ exclude line.
/**
* A throwaway repo on `branch` (default `main`) whose start commit holds a.mjs,
* d.mjs, old.mjs and the `seed` files (path -> text, staged with `add -f`).
* `commit(files, msg)` writes each path (null removes) and commits exactly
* those paths with `git add -f`; it returns the new sha.
*/
function pxbRepo({ branch = 'main', seed = {} } = {}) {
const base = mkdtempSync(join(tmpdir(), 'pre-existing-pxb-'));
made.push(base);
const repo = join(base, 'repo');
mkdirSync(repo, { recursive: true });
mkdirSync(join(base, 'hooks-none'));
g(repo, 'init', '-q', '-b', branch);
appendFileSync(join(repo, '.git', 'config'), '[user]\n\tname = Test\n\temail = test@example.invalid\n'
+ `[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "${join(base, 'hooks-none')}"\n`);
const commit = (files, msg) => {
for (const [rel, text] of Object.entries(files)) {
if (text === null) g(repo, 'rm', '-q', '--', rel);
else putIn(repo, rel, text);
}
const kept = Object.entries(files).filter(([, t]) => t !== null).map(([rel]) => rel);
if (kept.length > 0) g(repo, 'add', '-f', '--', ...kept);
g(repo, 'commit', '-q', '-m', msg);
return g(repo, 'rev-parse', 'HEAD');
};
const start = commit({ 'a.mjs': ten('a'), 'd.mjs': ten('d'), 'old.mjs': ten('o'), ...seed }, 'chore: start');
const reviewPath = join(repo, ...REVIEW_REL.split('/'));
return {
repo, start, reviewPath, commit,
write: (text) => putIn(repo, REVIEW_REL, text),
read: () => readFileSync(reviewPath, 'utf8'),
gate: () => spawnSync(process.execPath, [GATE, '--json', reviewPath, '--repo', repo], { encoding: 'utf8', env: childEnv() }),
finalize: () => spawnSync(process.execPath, [FINALIZE, '--review', reviewPath, '--repo', repo,
'--project', dirname(reviewPath), '--round', '1'], { encoding: 'utf8', env: childEnv() }),
};
}
// Copied from prxReview in tests/lib/pre-existing.test.mjs:123-187 (no round fields).
function pxbReview({ start, end, findings }) {
const list = findings.map((f, i) => ({
severity: f.severity, rule_key: f.rule_key ?? 'MISSING_TEST',
file: f.file, line: f.line, title: f.title ?? `t${i + 1}`, detail: `d${i + 1}`, recommended_action: 'Add a test.',
...('pre_existing' in f ? { pre_existing: f.pre_existing } : {}),
}));
const computed = computeFindingIds(list);
list.forEach((f, i) => { f.id = computed[i]; });
const ordered = list.map(({ id, ...rest }) => ({ id, ...rest }));
const counts = { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 };
for (const f of ordered) counts[f.severity] += 1;
const v = counts.BLOCKER > 0 ? 'BLOCK' : counts.MAJOR > 0 ? 'WARN' : 'ALLOW';
const json = JSON.stringify({ verdict: v, counts, allow_blocked_by: [], findings: ordered }, null, 2);
const sections = ['BLOCKER', 'MAJOR', 'MINOR', 'SUGGESTION'].map((sev) => {
const of = ordered.filter((f) => f.severity === sev);
if (of.length === 0) return '';
return `## Findings (${sev})\n\n${of.map((f) => `### ${f.id}\n\n- **Rule:** \`${f.rule_key}\`\n- **Location:** \`${f.file}:${f.line}\`\n- **Title:** ${f.title}\n`).join('\n')}\n`;
}).join('');
const steps = ordered.filter((f) => f.severity === 'BLOCKER' || f.severity === 'MAJOR')
.map((f, i) => `### Step R${i + 1}: fix ${f.title}\n\n- **Files:** \`${f.file}\`\n- **Source finding:** \`${f.id}\`\n- **Test first:** a test that fails today\n- **Verify:** re-run /trekreview\n- **On failure:** escalate\n`).join('\n');
return `---
type: trekreview
review_version: "1.2"
task: "t"
slug: p
project_dir: .claude/projects/2026-11-02-p/
brief_path: .claude/projects/2026-11-02-p/brief.md
scope_sha_start: ${start}
scope_sha_end: ${end}
reviewed_files_count: 3
verdict: ${v}
findings:
${ordered.map((f) => ` - ${f.id}`).join('\n')}
---
# Review: t
## Executive Summary
x
## Coverage
| File | Treatment | Reason |
|------|-----------|--------|
| a.mjs | deep-review | default |
${sections}## Declined to Judge
None.
Trimmed: 0
## Remediation Summary
x
## Remediation Steps
${steps || 'None.\n'}
\`\`\`json
${json}
\`\`\`
`;
}
const jsonOf = (text) => JSON.parse(text.slice(text.lastIndexOf('```json\n') + 8, text.lastIndexOf('```')));
/** Finalize a review of start..end holding `findings` (all MAJOR unless said); return the marks. */
function marksAfterFinalize(fx, { start, end }, findings) {
assert.equal(typeof finalizeMod.finalizeFile, 'function', 'review-finalize.mjs exports finalizeFile');
fx.write(pxbReview({ start, end, findings: findings.map((f) => ({ severity: 'MAJOR', ...f })) }));
const r = fx.finalize();
assert.equal(r.status, 0, r.stdout + r.stderr);
return jsonOf(fx.read()).findings.map((x) => `${x.file}:${x.line}=${x.pre_existing}`);
}
/** Run the gate on a one-MAJOR WARN review of start..end whose finding says `pre_existing: true`. */
function gateMarked(fx, { start, end }, finding) {
fx.write(pxbReview({ start, end, findings: [{ severity: 'MAJOR', pre_existing: true, ...finding }] }));
return fx.gate();
}
function assertUnanswered(r, what = '') {
assert.equal(r.status, 2, `${what}${r.stdout}${r.stderr}`);
assert.match(String(parse(r).reason), /^review-unanswered:/, `${what}${r.stdout}`);
}
function forkOf(fx, end) {
assert.equal(typeof preMod.forkPoint, 'function', 'pre-existing.mjs exports forkPoint');
const fp = preMod.forkPoint(fx.repo, end);
assert.equal(fp.ok, true, JSON.stringify(fp));
return fp.sha;
}
test('[PXB] a forged start one commit after the fork point: finalize marks nothing pre-existing and the gate exits 2 review-unanswered for a MAJOR on a line the first run commit added', () => {
const fx = pxbRepo();
const b = fx.start;
g(fx.repo, 'checkout', '-q', '-b', 'feat');
const c1 = fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: C1 changes a.mjs line 3');
const c2 = fx.commit({ 'd.mjs': ten('d', { 8: 'export const d8 = 88;' }) }, 'feat: C2 changes d.mjs');
// Control: with the true start B, the same MAJOR is not pre-existing either (C1 added its line).
assert.deepEqual(marksAfterFinalize(fx, { start: b, end: c2 }, [{ file: 'a.mjs', line: 3 }]), ['a.mjs:3=false']);
// The forged start C1: finalize marks nothing.
assert.deepEqual(marksAfterFinalize(fx, { start: c1, end: c2 }, [{ file: 'a.mjs', line: 3 }]), ['a.mjs:3=false']);
const fin = fx.gate();
assertUnanswered(fin, 'the finalized review: ');
// A mark written by hand is re-derived by the gate and refused.
assertUnanswered(gateMarked(fx, { start: c1, end: c2 }, { file: 'a.mjs', line: 3 }), 'a forged mark: ');
});
test('[PXB] a start after two approval-log and .claude/projects/ commits on the branch still binds: finalize marks a finding on an unchanged line pre-existing', () => {
const fx = pxbRepo();
const fork = fx.start;
g(fx.repo, 'checkout', '-q', '-b', 'feat');
fx.commit({ [LOG_REL]: '{"seq":1,"kind":"brief"}\n' }, 'chore(voyage): approval log');
const start = fx.commit({
'.claude/projects/2026-11-02-p/brief.md': '# Brief\n',
'.claude/projects/2026-11-02-p/plan.md': '# Plan\n',
}, 'chore(voyage): brief and plan');
const end = fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: change a.mjs line 3');
// Control: the tree diff from the fork point names only process files.
const names = g(fx.repo, 'diff', '--name-only', fork, start).split('\n');
assert.ok(names.includes(LOG_REL), names.join(','));
assert.ok(names.some((p) => p.startsWith('.claude/projects/')), names.join(','));
assert.deepEqual(marksAfterFinalize(fx, { start, end }, [{ file: 'a.mjs', line: 7 }, { file: 'a.mjs', line: 3 }]),
['a.mjs:7=true', 'a.mjs:3=false']);
});
test('[PXB] a start after a commit that changes .claude/hooks/x.sh, .claude/voyage/other.json, sub/.claude/projects/x.md, .claude/projectsX/x.md, or a source file beside the log does not bind', () => {
const variants = [
['.claude/hooks/x.sh', { '.claude/hooks/x.sh': 'echo x\n' }],
['.claude/voyage/other.json', { '.claude/voyage/other.json': '{}\n' }],
['sub/.claude/projects/x.md', { 'sub/.claude/projects/x.md': '# x\n' }],
['.claude/projectsX/x.md', { '.claude/projectsX/x.md': '# x\n' }],
['a source file beside the log', { '.claude/voyage/approvals.mjs': 'export const x = 1;\n' }],
['a source file committed with the log', { [LOG_REL]: '{"seq":1}\n', 'src/beside.mjs': 'export const y = 1;\n' }],
];
for (const [what, files] of variants) {
const fx = pxbRepo();
g(fx.repo, 'checkout', '-q', '-b', 'feat');
const start = fx.commit(files, `chore: ${what}`);
const end = fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: change a.mjs line 3');
const fork = forkOf(fx, end);
assert.equal(fork, fx.start, `${what}: the fork point is the start commit on main`);
assert.equal(typeof preMod.startBound, 'function', 'pre-existing.mjs exports startBound');
assert.equal(preMod.startBound(fx.repo, start, fork).ok, false, `${what}: the start is not bound`);
assert.deepEqual(marksAfterFinalize(fx, { start, end }, [{ file: 'a.mjs', line: 7 }]), ['a.mjs:7=false'], what);
}
});
test('[PXB] a start reached through a source commit and then a commit that changes only .claude/projects/ does not bind: the tree diff from the fork point holds the source change', () => {
const fx = pxbRepo();
g(fx.repo, 'checkout', '-q', '-b', 'feat');
fx.commit({ 'a.mjs': ten('a', { 5: 'export const a5 = 55;' }) }, 'feat: change a.mjs line 5');
const start = fx.commit({ '.claude/projects/2026-11-02-p/brief.md': '# Brief\n' }, 'chore(voyage): brief');
const end = fx.commit({ 'd.mjs': ten('d', { 8: 'export const d8 = 88;' }) }, 'feat: change d.mjs');
// Neither the start commit nor start..end touches a.mjs.
assert.equal(g(fx.repo, 'diff', '--name-only', `${start}^`, start), '.claude/projects/2026-11-02-p/brief.md');
assert.equal(g(fx.repo, 'diff', '--name-only', start, end), 'd.mjs');
const fork = forkOf(fx, end);
assert.equal(typeof preMod.startBound, 'function', 'pre-existing.mjs exports startBound');
assert.equal(preMod.startBound(fx.repo, start, fork).ok, false, 'the start is not bound');
assert.deepEqual(marksAfterFinalize(fx, { start, end }, [{ file: 'a.mjs', line: 5 }]), ['a.mjs:5=false']);
assertUnanswered(gateMarked(fx, { start, end }, { file: 'a.mjs', line: 5 }));
});
test('[PXB] a start whose one commit changes .claude/projects/p.md and src/x.mjs together does not bind', () => {
const fx = pxbRepo({ seed: { 'src/x.mjs': ten('x') } });
g(fx.repo, 'checkout', '-q', '-b', 'feat');
const start = fx.commit({
'.claude/projects/p.md': '# p\n',
'src/x.mjs': ten('x', { 2: 'export const x2 = 22;' }),
}, 'chore: a project file and a source file');
const end = fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: change a.mjs line 3');
const fork = forkOf(fx, end);
assert.equal(typeof preMod.startBound, 'function', 'pre-existing.mjs exports startBound');
assert.equal(preMod.startBound(fx.repo, start, fork).ok, false, 'the start is not bound');
assert.deepEqual(marksAfterFinalize(fx, { start, end }, [{ file: 'src/x.mjs', line: 7 }]), ['src/x.mjs:7=false']);
});
test('[PXB] a finding on .claude/voyage/approvals.jsonl or on a file under .claude/projects/ is never pre-existing, under a bound start', () => {
const notes = '.claude/projects/2026-11-02-p/notes.md';
const findings = [{ file: LOG_REL, line: 1 }, { file: notes, line: 1 }, { file: 'a.mjs', line: 7 }];
const want = [`${LOG_REL}:1=false`, `${notes}:1=false`, 'a.mjs:7=true'];
assert.equal(typeof preMod.startBound, 'function', 'pre-existing.mjs exports startBound');
// 1. The fork point itself: main holds the log and the project file.
const one = pxbRepo({ seed: { [LOG_REL]: '{"seq":1}\n', [notes]: '# Notes\n' } });
g(one.repo, 'checkout', '-q', '-b', 'feat');
const end1 = one.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: change a.mjs line 3');
const fork1 = forkOf(one, end1);
assert.equal(fork1, one.start);
assert.equal(preMod.startBound(one.repo, one.start, fork1).ok, true, 'Control: the fork point is bound');
assert.deepEqual(marksAfterFinalize(one, { start: one.start, end: end1 }, findings), want);
// 2. A later start whose own process commit added the cited lines.
const two = pxbRepo();
g(two.repo, 'checkout', '-q', '-b', 'feat');
const start2 = two.commit({ [LOG_REL]: '{"seq":1}\n', [notes]: '# Notes\n' }, 'chore(voyage): log and notes');
const end2 = two.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: change a.mjs line 3');
const fork2 = forkOf(two, end2);
assert.equal(preMod.startBound(two.repo, start2, fork2).ok, true, 'Control: the process-files start is bound');
assert.deepEqual(marksAfterFinalize(two, { start: start2, end: end2 }, findings), want);
});
test('[PXB] docs/HANDOVER-CONTRACTS.md\'s pre_existing row says it is never true unless scope_sha_start is bound to the fork point from main', () => {
const text = readFileSync(join(ROOT, 'docs', 'HANDOVER-CONTRACTS.md'), 'utf8');
const row = text.split('\n').find((l) => l.includes('per finding `pre_existing` (boolean:'));
assert.ok(row, 'the Handover 6 pre_existing row');
assert.ok(row.includes('the range did not add or change the cited line and `scope_sha_start` is bound to the fork point from main (the fork point itself, or a later commit reached through process files only); otherwise it is never true'), row);
});
test('[PXB] forkPoint is the fork commit before the merge, after a --no-ff merge into main, and after main moved on; master is used when there is no main; neither branch is ok:false', () => {
assert.equal(typeof preMod.forkPoint, 'function', 'pre-existing.mjs exports forkPoint');
const fx = pxbRepo();
const b = fx.start;
g(fx.repo, 'checkout', '-q', '-b', 'feat');
fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: C1');
const c2 = fx.commit({ 'd.mjs': ten('d', { 8: 'export const d8 = 88;' }) }, 'feat: C2');
assert.deepEqual(preMod.forkPoint(fx.repo, c2), { ok: true, sha: b, ref: 'refs/heads/main' }, 'before the merge');
g(fx.repo, 'checkout', '-q', 'main');
fx.commit({ 'old.mjs': ten('o', { 1: 'export const o1 = 11;' }) }, 'feat: main moves on');
assert.deepEqual(preMod.forkPoint(fx.repo, c2), { ok: true, sha: b, ref: 'refs/heads/main' }, 'after main moved on');
g(fx.repo, 'merge', '-q', '--no-ff', '-m', 'merge feat', 'feat');
assert.deepEqual(preMod.forkPoint(fx.repo, c2), { ok: true, sha: b, ref: 'refs/heads/main' }, 'after a --no-ff merge');
const ms = pxbRepo({ branch: 'master' });
g(ms.repo, 'checkout', '-q', '-b', 'feat');
const mEnd = ms.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: C1');
assert.deepEqual(preMod.forkPoint(ms.repo, mEnd), { ok: true, sha: ms.start, ref: 'refs/heads/master' }, 'master');
const none = pxbRepo({ branch: 'trunk' });
g(none.repo, 'checkout', '-q', '-b', 'feat');
const nEnd = none.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: C1');
assert.equal(preMod.forkPoint(none.repo, nEnd).ok, false, 'neither main nor master');
});
test('[PXB] a range that ends on main itself: finalize marks nothing pre-existing on a line the range did not change', () => {
const fx = pxbRepo();
const end = fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: straight on main');
assert.deepEqual(marksAfterFinalize(fx, { start: fx.start, end }, [{ file: 'a.mjs', line: 7 }]), ['a.mjs:7=false']);
assertUnanswered(gateMarked(fx, { start: fx.start, end }, { file: 'a.mjs', line: 7 }));
});
test('[PXB] after a --no-ff merge into main, with HEAD on main, decideOpenFindings with the true start leaves a MAJOR on an unchanged line out of the answer set', () => {
assert.equal(typeof gateMod.decideOpenFindings, 'function', 'verdict-gate.mjs exports decideOpenFindings');
const fx = pxbRepo();
g(fx.repo, 'checkout', '-q', '-b', 'feat');
const end = fx.commit({ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }) }, 'feat: C1');
g(fx.repo, 'checkout', '-q', 'main');
g(fx.repo, 'merge', '-q', '--no-ff', '-m', 'merge feat', 'feat');
assert.equal(g(fx.repo, 'rev-parse', 'HEAD^2'), end, 'HEAD is the --no-ff merge on main');
const text = pxbReview({ start: fx.start, end, findings: [{ severity: 'MAJOR', file: 'a.mjs', line: 7, pre_existing: true }] });
fx.write(text);
const d = gateMod.decideOpenFindings(text, { repo: fx.repo, reviewPath: fx.reviewPath });
assert.equal(d.exit, 0, JSON.stringify(d));
// Control: a MAJOR on the line the branch changed stays in the answer set.
const changed = pxbReview({ start: fx.start, end, findings: [{ severity: 'MAJOR', file: 'a.mjs', line: 3, pre_existing: true }] });
fx.write(changed);
assert.equal(gateMod.decideOpenFindings(changed, { repo: fx.repo, reviewPath: fx.reviewPath }).exit, 2);
});
test('[PXB] a start older than the fork point does not bind: main changes line L between that start and the fork point, the branch puts L back, and a MAJOR on L is not pre-existing; the gate exits 2 review-unanswered', () => {
const fx = pxbRepo();
const older = fx.start;
const fork = fx.commit({ 'a.mjs': ten('a', { 5: 'export const a5 = 55;' }) }, 'feat: main changes a.mjs line 5');
g(fx.repo, 'checkout', '-q', '-b', 'feat');
fx.commit({ 'a.mjs': ten('a') }, 'feat: put a.mjs line 5 back');
const end = fx.commit({ 'd.mjs': ten('d', { 8: 'export const d8 = 88;' }) }, 'feat: change d.mjs');
// The tree diff older..end does not show line 5.
assert.equal(g(fx.repo, 'diff', '--name-only', older, end), 'd.mjs');
assert.equal(forkOf(fx, end), fork);
assert.equal(typeof preMod.startBound, 'function', 'pre-existing.mjs exports startBound');
assert.equal(preMod.startBound(fx.repo, older, fork).ok, false, 'an older start is not bound');
assert.deepEqual(marksAfterFinalize(fx, { start: older, end }, [{ file: 'a.mjs', line: 5 }]), ['a.mjs:5=false']);
assertUnanswered(gateMarked(fx, { start: older, end }, { file: 'a.mjs', line: 5 }));
});
test('[PXB] /trekreview Phase 7 says the range counts only when scope_sha_start is bound to the fork point from main', () => {
const text = readFileSync(join(ROOT, 'commands', 'trekreview.md'), 'utf8').replace(/\s+/g, ' ');
const want = 'The range counts only when `scope_sha_start` is bound to the fork point from main (`main`, else `master`): '
+ 'the fork point itself, or a later commit reached through process files only (the approval log, `.claude/projects/`); '
+ 'otherwise, and for a range that ends on main itself, nothing is pre-existing.';
const phase7 = text.indexOf('## Phase 7');
const phase8 = text.indexOf('## Phase 8 ');
assert.ok(phase7 !== -1 && phase8 > phase7, 'Phase 7 and Phase 8 headings');
const at = text.indexOf(want);
assert.ok(at > phase7 && at < phase8, `Phase 7 holds: ${want}`);
});