// tests/lib/pre-existing.test.mjs // [PRX] fix round 4.5b2 part B, Step 5: a finding on a line the range did not // add or change is marked pre-existing (lib/review/review-finalize.mjs, from // git through lib/review/pre-existing.mjs), and the verdict gate re-derives the // mark from git: it never trusts it as written. A pre-existing MAJOR leaves the // answer set; a BLOCKER or a security finding counts in full whatever the mark; // every uncertain case (a renamed file, line 0, no scope_sha_start, a changed // line, a line next to a pure deletion, an uncommitted file) counts as new. // Tests run the CLIs (gate, finalize, fill-finding-ids, validator) in throwaway // git repos with no hooks; `changedLineSets` is also read directly for the // repo-config case. `keep:` cases hold at the base already and stay untagged. // Each tagged gate case carries a Control: the same finding on an unchanged // line exits 0, which only a gate that reads the mark can do. 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, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { computeFindingId, 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 FILL = join(ROOT, 'lib', 'review', 'fill-finding-ids.mjs'); const VALIDATOR = join(ROOT, 'lib', 'validators', 'review-validator.mjs'); const REVIEW_REL = '.claude/projects/2026-11-02-p/review.md'; // Loaded so that each new assertion fails on its own while the module is absent. let preMod = {}; try { preMod = await import(pathToFileURL(PRE).href); } catch (e) { if (existsSync(PRE)) throw e; } 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'; /** * A throwaway repo. The start commit holds a.mjs, d.mjs and old.mjs (ten lines * each). `rounds` are later commits, each a map of path → text (null removes, * `{ mv: from }` renames). The default single round: * - a.mjs line 3 changed; * - d.mjs line 5 removed (a pure deletion: new lines 4 and 5 sit next to it); * - old.mjs renamed to moved.mjs, unchanged; * - n.mjs added. * `config` lines go into .git/config before the rounds are committed. */ function prxRepo({ config = [], rounds } = {}) { const base = mkdtempSync(join(tmpdir(), 'pre-existing-prx-')); made.push(base); const repo = join(base, 'repo'); mkdirSync(repo, { recursive: true }); mkdirSync(join(base, 'hooks-none')); g(repo, 'init', '-q', '-b', 'main'); 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`); appendFileSync(join(repo, '.git', 'info', 'exclude'), '.claude/projects/\n'); putIn(repo, 'a.mjs', ten('a')); putIn(repo, 'd.mjs', ten('d')); putIn(repo, 'old.mjs', ten('o')); g(repo, 'add', '-A'); g(repo, 'commit', '-q', '-m', 'chore: start'); const start = g(repo, 'rev-parse', 'HEAD'); g(repo, 'checkout', '-q', '-b', 'feat'); for (const c of config) g(repo, 'config', ...c); const plan = rounds ?? [{ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }), 'd.mjs': ten('d', { 5: null }), 'moved.mjs': { mv: 'old.mjs' }, 'n.mjs': 'export const n = 1;\n', }]; const shas = []; plan.forEach((files, i) => { for (const [rel, text] of Object.entries(files)) { if (text === null) g(repo, 'rm', '-q', rel); else if (typeof text === 'object') g(repo, 'mv', text.mv, rel); else putIn(repo, rel, text); } g(repo, 'add', '-A'); g(repo, 'commit', '-q', '-m', `feat: round ${i + 1}`); shas.push(g(repo, 'rev-parse', 'HEAD')); }); const reviewPath = join(repo, ...REVIEW_REL.split('/')); return { repo, start, shas, end: shas.at(-1), reviewPath, 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: (extra = []) => spawnSync(process.execPath, [FINALIZE, '--review', reviewPath, '--repo', repo, '--project', dirname(reviewPath), '--round', '1', ...extra], { encoding: 'utf8', env: childEnv() }), fill: () => spawnSync(process.execPath, [FILL, reviewPath], { encoding: 'utf8', env: childEnv() }), validate: () => spawnSync(process.execPath, [VALIDATOR, '--strict', reviewPath], { encoding: 'utf8', env: childEnv() }), }; } /** * A review_version 1.2 review of `start..end`. `findings` are * [{severity, file, line, rule_key?, engine?, pre_existing?, title?}]; ids are * computed over (file, line, rule_key) unless `ids` names them. `start: null` * writes `scope_sha_start: null`; `round`/`roundBase` add the round fields. */ function prxReview({ start, end, findings, verdict, ids, round, roundBase }) { const list = findings.map((f, i) => ({ severity: f.severity, rule_key: f.rule_key ?? 'MISSING_TEST', ...(f.engine ? { engine: f.engine } : {}), 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 = ids ?? 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 = verdict ?? (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 === null ? 'null' : start} scope_sha_end: ${end} reviewed_files_count: 3 verdict: ${v} ${round ? `round: ${round}\nround_base: ${roundBase ?? 'null'}\n` : ''}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('```'))); /** Write a one-finding WARN review of the fixture's range and run the gate on it. */ function gateOn(fx, finding, opts = {}) { fx.write(prxReview({ start: fx.start, end: fx.end, findings: [{ severity: 'MAJOR', ...finding }], ...opts })); return fx.gate(); } function assertUnanswered(r) { assert.equal(r.status, 2, r.stdout + r.stderr); assert.match(String(parse(r).reason), /^review-unanswered:/, r.stdout); } function assertControl(fx, finding = { file: 'a.mjs', line: 7, pre_existing: true }) { const r = gateOn(fx, finding); assert.equal(r.status, 0, `Control: a MAJOR marked pre-existing on an unchanged line opens the gate: ${r.stdout}${r.stderr}`); } test('[PRX] a MAJOR outside the -U0 added or changed lines is marked pre-existing and leaves the answer set', () => { assert.ok(existsSync(FINALIZE), 'lib/review/review-finalize.mjs must exist'); const fx = prxRepo(); fx.write(prxReview({ start: fx.start, end: fx.end, findings: [{ severity: 'MAJOR', file: 'a.mjs', line: 7, title: 'An old defect' }] })); const f = fx.finalize(); assert.equal(f.status, 0, f.stdout + f.stderr); const text = fx.read(); const payload = jsonOf(text); assert.equal(payload.findings[0].pre_existing, true, text); assert.deepEqual(payload.pre_existing_counts, { BLOCKER: 0, MAJOR: 1, MINOR: 0, SUGGESTION: 0 }); assert.equal(payload.findings[0].severity, 'MAJOR', 'the mark never lowers a finding'); const pre = text.search(/^## Pre-existing\s*$/m); const declined = text.search(/^## Declined to Judge\s*$/m); assert.ok(pre !== -1 && pre < declined, `## Pre-existing stands just before ## Declined to Judge:\n${text}`); assert.ok(text.slice(pre, declined).includes('An old defect'), text.slice(pre, declined)); assert.ok(!/^## Answers/m.test(text), 'no answer was written'); const r = fx.gate(); assert.equal(r.status, 0, r.stdout + r.stderr); }); test('[PRX] a finding in a renamed file, on line 0, with no scope_sha_start, on a changed line or next to a pure deletion counts as new', () => { assert.equal(typeof preMod.changedLineSets, 'function', 'pre-existing.mjs exports changedLineSets'); const fx = prxRepo(); assertControl(fx); const cases = [ ['a renamed file', { file: 'moved.mjs', line: 5, pre_existing: true }], ['line 0', { file: 'a.mjs', line: 0, pre_existing: true }], ['a changed line', { file: 'a.mjs', line: 3, pre_existing: true }], ['the line before a pure deletion', { file: 'd.mjs', line: 4, pre_existing: true }], ['the line after a pure deletion', { file: 'd.mjs', line: 5, pre_existing: true }], ['a new file', { file: 'n.mjs', line: 1, pre_existing: true }], ]; for (const [what, finding] of cases) { const r = gateOn(fx, finding); assert.equal(r.status, 2, `${what}: ${r.stdout}${r.stderr}`); assert.match(String(parse(r).reason), /^review-unanswered:/, `${what}: ${r.stdout}`); } const none = gateOn(fx, { file: 'a.mjs', line: 7, pre_existing: true }, { start: null }); assert.equal(none.status, 2, `no scope_sha_start: ${none.stdout}${none.stderr}`); assert.match(String(parse(none).reason), /^review-unanswered:/, none.stdout); // Control: a line well away from the deletion is still pre-existing (the file is not 'all'). assertControl(fx, { file: 'd.mjs', line: 8, pre_existing: true }); }); test('[PRX] finalize marks none of the uncertain cases, and no finding in a file with uncommitted changes', () => { assert.ok(existsSync(FINALIZE), 'lib/review/review-finalize.mjs must exist'); const fx = prxRepo(); const findings = [ { severity: 'MAJOR', file: 'moved.mjs', line: 5 }, { severity: 'MAJOR', file: 'a.mjs', line: 3 }, { severity: 'MAJOR', file: 'd.mjs', line: 4 }, { severity: 'MAJOR', file: 'd.mjs', line: 5 }, { severity: 'MINOR', file: 'a.mjs', line: 0 }, { severity: 'MAJOR', file: 'd.mjs', line: 8 }, ]; fx.write(prxReview({ start: fx.start, end: fx.end, findings })); const f = fx.finalize(); assert.equal(f.status, 0, f.stdout + f.stderr); assert.deepEqual(jsonOf(fx.read()).findings.map((x) => x.pre_existing), [false, false, false, false, false, true]); // An uncommitted change in d.mjs: its findings are not marked. putIn(fx.repo, 'd.mjs', ten('d', { 5: null, 9: 'export const d10 = 0;' })); fx.write(prxReview({ start: fx.start, end: fx.end, findings: [{ severity: 'MAJOR', file: 'd.mjs', line: 8 }] })); const u = fx.finalize(); assert.equal(u.status, 0, u.stdout + u.stderr); assert.equal(jsonOf(fx.read()).findings[0].pre_existing, false, fx.read()); // No scope_sha_start: nothing is marked. fx.write(prxReview({ start: null, end: fx.end, findings: [{ severity: 'MAJOR', file: 'a.mjs', line: 7 }] })); const n = fx.finalize(); assert.equal(n.status, 0, n.stdout + n.stderr); assert.equal(jsonOf(fx.read()).findings[0].pre_existing, false, fx.read()); }); test('[PRX] in a round-2 review whose scope_sha_start is the run\'s start, a MAJOR on a line round 1 added counts as new', () => { assert.equal(typeof preMod.changedLineSets, 'function', 'pre-existing.mjs exports changedLineSets'); const fx = prxRepo({ rounds: [ { 'a.mjs': ten('a', { 5: 'export const a5 = 55;' }) }, { 'a.mjs': ten('a', { 5: 'export const a5 = 55;', 9: 'export const a9 = 99;' }) }, ] }); const [round1] = fx.shas; const opts = { round: 2, roundBase: round1 }; const r = gateOn(fx, { file: 'a.mjs', line: 5, pre_existing: true }, opts); assertUnanswered(r); // Control: an unchanged line of the same file is pre-existing in round 2. const c = gateOn(fx, { file: 'a.mjs', line: 7, pre_existing: true }, opts); assert.equal(c.status, 0, `Control: ${c.stdout}${c.stderr}`); // Finalize marks by the run range too, whatever --round-base says. fx.write(prxReview({ start: fx.start, end: fx.end, findings: [{ severity: 'MAJOR', file: 'a.mjs', line: 5 }], ...opts })); const f = fx.finalize(['--round-base', round1]); assert.equal(f.status, 0, f.stdout + f.stderr); assert.equal(jsonOf(fx.read()).findings[0].pre_existing, false, fx.read()); }); test('[PRX] a pre-existing security MAJOR (CODE_REVIEW_DEFECT from security-review) still exits 2 with review-unanswered:', () => { const fx = prxRepo(); // Control: the same key from the code-review engine is pre-existing and opens the gate. assertControl(fx, { file: 'a.mjs', line: 7, rule_key: 'CODE_REVIEW_DEFECT', engine: 'code-review', pre_existing: true }); const r = gateOn(fx, { file: 'a.mjs', line: 7, rule_key: 'CODE_REVIEW_DEFECT', engine: 'security-review', pre_existing: true }); assertUnanswered(r); }); test('[PRX] after finalize every id is the computed one and the gate reads the file', () => { assert.ok(existsSync(FINALIZE), 'lib/review/review-finalize.mjs must exist'); const fx = prxRepo(); const findings = [ { severity: 'MAJOR', file: 'a.mjs', line: 7, title: 'An old defect' }, { severity: 'MINOR', file: 'a.mjs', line: 3, title: 'A new nit' }, ]; // The MAJOR carries an id over an old line (8): finalize recomputes it. const stale = computeFindingId('a.mjs', 8, 'MISSING_TEST'); const good = computeFindingId('a.mjs', 3, 'MISSING_TEST'); fx.write(prxReview({ start: fx.start, end: fx.end, findings, ids: [stale, good] })); const f = fx.finalize(); assert.equal(f.status, 0, f.stdout + f.stderr); const text = fx.read(); const want = computeFindingId('a.mjs', 7, 'MISSING_TEST'); assert.deepEqual(jsonOf(text).findings.map((x) => x.id), [want, good]); assert.ok(!text.includes(stale), `no stale id is left anywhere:\n${text}`); assert.ok(text.includes(` - ${want}\n`) && text.includes(`### ${want}\n`) && text.includes(`**Source finding:** \`${want}\``), text); const fill = fx.fill(); assert.equal(fill.status, 0, `fill-finding-ids has nothing left to fill: ${fill.stdout}${fill.stderr}`); assert.equal(fx.read(), text, 'fill-finding-ids leaves the finalized file unchanged'); const v = fx.validate(); assert.equal(v.status, 0, `review-validator --strict: ${v.stdout}${v.stderr}`); const r = fx.gate(); assert.equal(r.status, 0, r.stdout + r.stderr); assert.doesNotMatch(String(parse(r).reason), /differ/, r.stdout); }); test('[PRX] a forged pre_existing mark on an added line still needs an answer', () => { const fx = prxRepo(); assertControl(fx); assertUnanswered(gateOn(fx, { file: 'a.mjs', line: 3, pre_existing: true })); assertUnanswered(gateOn(fx, { file: 'n.mjs', line: 1, pre_existing: true })); // A path the diff cannot name the same way is never pre-existing. assertUnanswered(gateOn(fx, { file: './a.mjs', line: 3, pre_existing: true })); }); test('[PRX] a path or line that is not in the tree at scope_sha_end counts as new, at the gate and in finalize', () => { assert.ok(existsSync(FINALIZE), 'lib/review/review-finalize.mjs must exist'); // The default round plus a new sub/x.mjs (ten lines). a.mjs has ten lines at the end. const fx = prxRepo({ rounds: [{ 'a.mjs': ten('a', { 3: 'export const a3 = 33;' }), 'd.mjs': ten('d', { 5: null }), 'moved.mjs': { mv: 'old.mjs' }, 'n.mjs': 'export const n = 1;\n', 'sub/x.mjs': ten('x'), }] }); // Control: the last line of a.mjs is in the file and unchanged, so it is pre-existing. assertControl(fx, { file: 'a.mjs', line: 10, pre_existing: true }); const cases = [ ['a case variant of a changed file (the disk may be case-insensitive)', { file: 'A.mjs', line: 3 }], ['a file missing at scope_sha_end', { file: 'zzz.mjs', line: 5 }], ['a subdirectory-relative path to the added sub/x.mjs', { file: 'x.mjs', line: 3 }], ['a case variant of the added sub/x.mjs', { file: 'Sub/x.mjs', line: 3 }], ['a line past the end of the file', { file: 'a.mjs', line: 500 }], ['the line just past the end of the file', { file: 'a.mjs', line: 11 }], ]; for (const [what, finding] of cases) { const r = gateOn(fx, { ...finding, pre_existing: true }); assert.equal(r.status, 2, `${what}: ${r.stdout}${r.stderr}`); assert.match(String(parse(r).reason), /^review-unanswered:/, `${what}: ${r.stdout}`); } const findings = [...cases.map(([, f]) => ({ severity: 'MAJOR', ...f })), { severity: 'MAJOR', file: 'a.mjs', line: 10 }]; fx.write(prxReview({ start: fx.start, end: fx.end, findings })); const f = fx.finalize(); assert.equal(f.status, 0, f.stdout + f.stderr); const marks = jsonOf(fx.read()).findings.map((x) => `${x.file}:${x.line}=${x.pre_existing}`); assert.deepEqual(marks, ['A.mjs:3=false', 'zzz.mjs:5=false', 'x.mjs:3=false', 'Sub/x.mjs:3=false', 'a.mjs:500=false', 'a.mjs:11=false', 'a.mjs:10=true']); }); test('[PRX] a symlink to a file the range changed is not a regular file: a finding through it counts as new', () => { assert.ok(existsSync(FINALIZE), 'lib/review/review-finalize.mjs must exist'); // The fixture's own range is not used: the run starts at a commit that adds // lnk.mjs -> a.mjs, and ends at one that changes line 1 of a.mjs. The link is // unchanged in the range, so only the tree mode at the end tells it apart. // Line 1 is cited because the link's blob ("a.mjs") is one line long. const fx = prxRepo(); symlinkSync('a.mjs', join(fx.repo, 'lnk.mjs')); g(fx.repo, 'add', '--', 'lnk.mjs'); g(fx.repo, 'commit', '-q', '-m', 'chore: link'); const start = g(fx.repo, 'rev-parse', 'HEAD'); g(fx.repo, 'branch', '-f', 'main', start); assert.equal(g(fx.repo, 'ls-tree', start, '--', 'lnk.mjs').split(' ')[0], '120000', 'lnk.mjs is committed as a symlink'); putIn(fx.repo, 'a.mjs', ten('a', { 1: 'export const a1 = 11;' })); g(fx.repo, 'add', '--', 'a.mjs'); g(fx.repo, 'commit', '-q', '-m', 'feat: change a.mjs line 1'); const end = g(fx.repo, 'rev-parse', 'HEAD'); const range = { start, end }; // Control: an unchanged line of a.mjs is pre-existing in this range. const c = gateOn(fx, { file: 'a.mjs', line: 7, pre_existing: true }, range); assert.equal(c.status, 0, `Control: ${c.stdout}${c.stderr}`); // Control: the changed line through its real path counts as new. assertUnanswered(gateOn(fx, { file: 'a.mjs', line: 1, pre_existing: true }, range)); assertUnanswered(gateOn(fx, { file: 'lnk.mjs', line: 1, pre_existing: true }, range)); fx.write(prxReview({ ...range, findings: [ { severity: 'MAJOR', file: 'lnk.mjs', line: 1 }, { severity: 'MAJOR', file: 'a.mjs', line: 7 }, ] })); const f = fx.finalize(); assert.equal(f.status, 0, f.stdout + f.stderr); const marks = jsonOf(fx.read()).findings.map((x) => `${x.file}:${x.line}=${x.pre_existing}`); assert.deepEqual(marks, ['lnk.mjs:1=false', 'a.mjs:7=true']); }); test('[PRX] repo diff config does not change the line sets', () => { assert.equal(typeof preMod.changedLineSets, 'function', 'pre-existing.mjs exports changedLineSets'); const view = (s) => ({ ok: s.ok, files: Object.fromEntries([...s.files].filter(([, v]) => v === 'all' || v.size > 0) .map(([k, v]) => [k, v === 'all' ? 'all' : [...v].sort((x, y) => x - y)]).sort()), renamed: [...s.renamed].sort(), }); const want = { ok: true, files: { 'a.mjs': [3], 'd.mjs': [4, 5], 'n.mjs': 'all' }, renamed: ['moved.mjs', 'old.mjs'] }; const plain = prxRepo(); assert.deepEqual(view(preMod.changedLineSets(plain.repo, plain.start, plain.end)), want); const fx = prxRepo({ config: [['diff.renames', 'false'], ['color.diff', 'always'], ['diff.noprefix', 'true']] }); assert.deepEqual(view(preMod.changedLineSets(fx.repo, fx.start, fx.end)), want); assertControl(fx); assertUnanswered(gateOn(fx, { file: 'moved.mjs', line: 5, pre_existing: true })); assertUnanswered(gateOn(fx, { file: 'a.mjs', line: 3, pre_existing: true })); }); test('keep: a pre-existing BLOCKER still exits 1', () => { const fx = prxRepo(); fx.write(prxReview({ start: fx.start, end: fx.end, findings: [{ severity: 'BLOCKER', file: 'a.mjs', line: 7, pre_existing: true }] })); const r = fx.gate(); assert.equal(r.status, 1, r.stdout + r.stderr); }); test('keep: the verdict gate accepts the pre_existing field without exit 2', () => { const fx = prxRepo(); fx.write(prxReview({ start: fx.start, end: fx.end, findings: [ { severity: 'MINOR', file: 'a.mjs', line: 7, pre_existing: true }, { severity: 'MINOR', file: 'a.mjs', line: 3, pre_existing: false }, ] })); const r = fx.gate(); assert.equal(r.status, 0, r.stdout + r.stderr); });