// 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 = ;`; `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}`); });