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

446 lines
22 KiB
JavaScript

// 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 <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';
/**
* 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);
});