voyage/tests/lib/security-engine.test.mjs
Kjell Tore Guttormsen f820081bdc
refactor(review): simplify pass over E (reuse, simplification, efficiency)
Quality only, no behaviour change (four cleanup reviewers, one implementer;
npm test 2480/0 on the working tree):

- threat-model.mjs splitBacked(): the one known_outside backing check of
  plan-gate (record and bare gate), verdict-gate and review-finalize, with
  one why-text; the engines' security keys derived from ENGINES; isSecurity.
- coordinator-contract: "not a security engine" read from ENGINES (a string
  engine only), not the code-review literal.
- review-rounds owns trailingBlock, findingsBlockSha, isMajor, isBlocker;
  review-answers imports them (review-rounds still imports nothing from it).
  prepare keeps the archive bytes it read instead of reading the previous
  archive again.
- engine-compare exports rowIn and payloadEngine; finalize uses them.
- plan-gate: one accepted_open builder; the stop-rule check hoisted; one
  top-level lookup for --record --brief.
- security-engine: notFullShas(), runCli's usage flag, readScopeList shared
  with engine-findings; findReport and reviewRoundSummary module-private;
  two dead defaults gone.
- tests: shared plan-gate harness and threat-model brief helpers; the [CNV]
  table cases call convertReport in-process, one CLI case per exit path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:27:59 +01:00

533 lines
28 KiB
JavaScript

// tests/lib/security-engine.test.mjs
// E 3.1 (decisions 175, 196, 219): the security engine of /trekreview.
// choose - which engine reviews security: Claude Security when available
// and base..head has committed changes; else /security-review when
// origin/HEAD resolves and origin/HEAD...HEAD is non-empty; else
// none (no-engine, no-origin, empty-diff) = "not measured", never 0.
// A git failure or bad usage is exit 2, never a reason.
// convert - Claude Security's report folder turned into a reviewer payload:
// confidence `high` -> engine findings, the rest -> `below_threshold`,
// never in `findings`; a scan that cannot be trusted is exit 2.
// ENGINES - claude-security is an engine of its own, and the action written
// for a finding names the engine that reported it.
// none - no engine chosen: the expected reviewer `security-engine` never
// reports, so the verdict gate exits 2, never ALLOW.
// choose runs the real CLI in throwaway git repos with no hooks and no global
// or system git config.
import { after, test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { appendFileSync, copyFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { childEnv } from '../helpers/child-env.mjs';
import { record, writeReport } from '../helpers/claude-security-report.mjs';
import { convertReport, NO_ENGINE_REVIEWER } from '../../lib/review/security-engine.mjs';
import { ENGINES, toReviewerPayload } from '../../lib/review/engine-findings.mjs';
import { runContract } from '../../lib/review/coordinator-contract.mjs';
import { decideVerdict } from '../../lib/review/verdict-gate.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const CLI = join(ROOT, 'lib', 'review', 'security-engine.mjs');
const GIT_ENV = { GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_NOSYSTEM: '1' };
const HEX40 = /^[0-9a-f]{40}$/;
const made = [];
after(() => { for (const b of made) rmSync(b, { recursive: true, force: true }); });
const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', env: childEnv(GIT_ENV) });
const parse = (r) => { try { return JSON.parse(r.stdout); } catch { return {}; } };
function g(cwd, ...args) {
const r = spawnSync('git', args, { cwd, encoding: 'utf8', env: childEnv(GIT_ENV) });
if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
}
/** A throwaway repo with two commits (start, end) that change a.mjs; no origin remote. */
function repo() {
const base = mkdtempSync(join(tmpdir(), 'security-engine-'));
made.push(base);
const dir = join(base, 'repo');
mkdirSync(dir, { recursive: true });
mkdirSync(join(base, 'hooks-none'));
g(dir, 'init', '-q', '-b', 'main');
appendFileSync(join(dir, '.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`);
writeFileSync(join(dir, 'a.mjs'), 'export const a = 1;\n');
g(dir, 'add', 'a.mjs');
g(dir, 'commit', '-q', '-m', 'chore: start');
const start = g(dir, 'rev-parse', 'HEAD');
writeFileSync(join(dir, 'a.mjs'), 'export const a = 2;\n');
g(dir, 'add', 'a.mjs');
g(dir, 'commit', '-q', '-m', 'feat: the work');
const end = g(dir, 'rev-parse', 'HEAD');
return {
base, dir, start, end,
/** An `origin` remote (never fetched) whose HEAD points at `sha`. */
originAt(sha) {
g(dir, 'remote', 'add', 'origin', join(base, 'nowhere.git'));
g(dir, 'update-ref', 'refs/remotes/origin/main', sha);
g(dir, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
},
/** An `origin` remote with no remote-tracking refs at all. */
originWithoutHead() {
g(dir, 'remote', 'add', 'origin', join(base, 'nowhere.git'));
},
};
}
const choose = (fx, { base = fx.start, head = fx.end, cs = 'missing' } = {}) =>
run(['choose', '--repo', fx.dir, '--base', base, '--head', head, '--claude-security', cs]);
function decided(r) {
assert.equal(r.status, 0, `choose decides: ${r.stdout}${r.stderr}`);
const j = parse(r);
assert.deepEqual(Object.keys(j).sort(), ['engine', 'reason'], `the output is {engine, reason}: ${r.stdout}`);
return j;
}
// ---- choose ----------------------------------------------------------------------
test('[CHS] Claude Security available and base..head has committed changes: claude-security', () => {
const fx = repo();
assert.deepEqual(decided(choose(fx, { cs: 'available' })), { engine: 'claude-security', reason: 'claude-security-available' });
});
test('[CHS] Claude Security missing, origin/HEAD behind HEAD: security-review', () => {
const fx = repo();
fx.originAt(fx.start);
assert.deepEqual(decided(choose(fx)), { engine: 'security-review', reason: 'claude-security-missing' });
});
test('[CHS] Claude Security available but base..head empty, origin/HEAD behind HEAD: security-review', () => {
const fx = repo();
fx.originAt(fx.start);
assert.deepEqual(decided(choose(fx, { base: fx.end, cs: 'available' })), { engine: 'security-review', reason: 'claude-security-empty-diff' });
});
test('[CHS] origin/HEAD equals HEAD and Claude Security is missing: no engine, empty-diff (a pushed main is not measured, never 0)', () => {
const fx = repo();
fx.originAt(fx.end);
assert.deepEqual(decided(choose(fx)), { engine: null, reason: 'empty-diff' });
});
test('[CHS] Claude Security available, base..head empty, no origin/HEAD: no engine, empty-diff', () => {
const fx = repo();
assert.deepEqual(decided(choose(fx, { base: fx.end, cs: 'available' })), { engine: null, reason: 'empty-diff' });
});
test('[CHS] only committed changes count: an uncommitted edit on top of an empty range is still empty-diff', () => {
const fx = repo();
writeFileSync(join(fx.dir, 'a.mjs'), 'export const a = 3;\n');
assert.deepEqual(decided(choose(fx, { base: fx.end, cs: 'available' })), { engine: null, reason: 'empty-diff' });
});
test('[CHS] no origin remote and Claude Security missing: no engine, no-engine', () => {
const fx = repo();
assert.deepEqual(decided(choose(fx)), { engine: null, reason: 'no-engine' });
});
test('[CHS] an origin remote whose HEAD does not resolve, Claude Security missing: no engine, no-origin', () => {
const fx = repo();
fx.originWithoutHead();
assert.deepEqual(decided(choose(fx)), { engine: null, reason: 'no-origin' });
});
test('[CHS] a git failure is exit 2 (could not run), never a reason', () => {
const fx = repo();
const plain = join(fx.base, 'plain');
mkdirSync(plain);
const notRepo = run(['choose', '--repo', plain, '--base', fx.start, '--head', fx.end, '--claude-security', 'available']);
assert.equal(notRepo.status, 2, `not a git repository: ${notRepo.stdout}${notRepo.stderr}`);
assert.equal(notRepo.stdout, '', 'nothing on stdout');
const unknown = 'f'.repeat(40);
for (const [base, head] of [[unknown, fx.end], [fx.start, unknown]]) {
const r = choose(fx, { base, head, cs: 'available' });
assert.equal(r.status, 2, `a sha that does not resolve: ${r.stdout}${r.stderr}`);
assert.equal(r.stdout, '');
}
});
test('[CHS] bad usage is exit 2', () => {
const fx = repo();
const cases = [
['no subcommand', []],
['an unknown subcommand', ['pick', '--repo', fx.dir]],
['--claude-security missing', ['choose', '--repo', fx.dir, '--base', fx.start, '--head', fx.end]],
['--claude-security maybe', ['choose', '--repo', fx.dir, '--base', fx.start, '--head', fx.end, '--claude-security', 'maybe']],
['--base a ref name', ['choose', '--repo', fx.dir, '--base', 'HEAD~1', '--head', fx.end, '--claude-security', 'available']],
['--base an option', ['choose', '--repo', fx.dir, '--base', '--output=/tmp/x', '--head', fx.end, '--claude-security', 'available']],
['--repo missing', ['choose', '--base', fx.start, '--head', fx.end, '--claude-security', 'available']],
['an unknown flag', ['choose', '--repo', fx.dir, '--base', fx.start, '--head', fx.end, '--claude-security', 'available', '--force']],
];
for (const [name, args] of cases) {
const r = run(args);
assert.equal(r.status, 2, `${name}: exit 2, got ${r.status}: ${r.stdout}${r.stderr}`);
assert.equal(r.stdout, '', `${name}: nothing on stdout`);
}
});
// ---- convert ---------------------------------------------------------------------
const BASE = 'a'.repeat(40);
const HEAD = 'b'.repeat(40);
function reportDir() {
const d = mkdtempSync(join(tmpdir(), 'security-engine-report-'));
made.push(d);
return d;
}
const MEDIUM = record({ id: 'F3', title: 'Path joined from a query value', file: 'src/run.mjs', line: 30, confidence: 'medium', cwe_id: 'CWE-22', category: 'Path Traversal' });
const LOW = record({ id: 'F4', title: 'Token compared with ===', file: 'src/run.mjs', line: 44, confidence: 'low', severity: 'LOW', cwe_id: 'CWE-208' });
const OUTSIDE = record({ id: 'F2', title: 'Old helper leaks a handle path', file: 'lib/old.mjs', line: 4 });
test('[CNV] a verified changes scan of base..head: confidence high becomes an engine finding, the rest go to below_threshold and never into findings', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, records: [record(), OUTSIDE, MEDIUM, LOW] });
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD, scopeFiles: ['src/run.mjs', 'src/other.mjs'] });
assert.equal(r.exit, 0, r.reason);
const p = r.payload;
assert.equal(p.reviewer, 'claude-security');
assert.equal(p.findings.length, 1, JSON.stringify(p.findings.map((f) => f.title)));
const [f] = p.findings;
assert.deepEqual([f.rule_key, f.severity, f.engine, f.file, f.line], ['SECURITY_REVIEW_VULNERABILITY', 'BLOCKER', 'claude-security', 'src/run.mjs', 12]);
assert.equal(f.title, record().title, 'summary <- title');
assert.ok(f.detail.includes(record().exploit_scenario), `failure_scenario <- exploit_scenario: ${f.detail}`);
assert.ok(f.detail.includes('CWE-78'), `category <- cwe_id: ${f.detail}`);
assert.equal(f.recommended_action, record().recommendation, 'fix <- recommendation');
assert.equal(f.brief_ref, ENGINES['claude-security'].brief_ref);
assert.match(f.id, HEX40);
assert.ok(!('rule_key' in record()), 'the record carries no narrower key');
assert.deepEqual(p.out_of_scope.map((o) => [o.file, o.engine]), [['lib/old.mjs', 'claude-security']], 'a high finding outside the scope is listed, never dropped');
assert.deepEqual(p.below_threshold.map((b) => b.title).sort(), [LOW.title, MEDIUM.title].sort(), 'medium and low go to below_threshold');
for (const b of p.below_threshold) assert.equal(b.engine, 'claude-security');
const inFindings = new Set([...p.findings, ...p.out_of_scope].map((x) => x.title));
for (const b of p.below_threshold) assert.ok(!inFindings.has(b.title), `${b.title} is never in findings or out_of_scope`);
});
test('[CNV] the CLI prints the payload and exits 0', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, records: [record(), MEDIUM] });
const scope = join(dir, 'scope.txt');
writeFileSync(scope, 'src/run.mjs\n');
const r = run(['convert', '--report-dir', dir, '--base', BASE, '--head', HEAD, '--scope', scope]);
assert.equal(r.status, 0, r.stdout + r.stderr);
const p = parse(r);
assert.equal(p.reviewer, 'claude-security');
assert.equal(p.findings.length, 1);
assert.equal(p.below_threshold.length, 1);
});
test('[CNV] a -dirty stamp counts', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, dirty: true });
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD });
assert.equal(r.exit, 0, r.reason);
assert.equal(r.payload.findings.length, 1);
});
test('[CNV] the newest folder whose stamp matches is read; a newer folder of another commit is passed over', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, ts: '20261108-100000', records: [record({ title: 'older scan' })] });
writeReport(dir, { head: HEAD, base: BASE, ts: '20261108-110000', records: [record({ title: 'newest matching scan' })] });
writeReport(dir, { head: 'c'.repeat(40), base: BASE, ts: '20261108-120000', records: [record({ title: 'another commit' })] });
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD });
assert.equal(r.exit, 0, r.reason);
assert.deepEqual(r.payload.findings.map((f) => f.title), ['newest matching scan']);
});
test('[CNV] a finding file relative to the scan root is made repo-relative with the stamp\'s scan_prefix', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, records: [record({ file: 'run.mjs' })], stamp: (s) => ({ ...s, scan_prefix: 'src/' }) });
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD, scopeFiles: ['src/run.mjs'] });
assert.equal(r.exit, 0, r.reason);
assert.deepEqual(r.payload.findings.map((f) => f.file), ['src/run.mjs']);
});
test('[CNV] a scan that found nothing is a report: exit 0, no findings', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, records: [] });
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD });
assert.equal(r.exit, 0, r.reason);
assert.deepEqual([r.payload.findings, r.payload.out_of_scope, r.payload.below_threshold], [[], [], []]);
});
test('[CNV] not measured: exit 2, the reason on stderr, nothing on stdout', () => {
const setV = (key, value) => (s) => ({ ...s, verification: { ...s.verification, [key]: value } });
const dropV = (key) => (s) => { const v = { ...s.verification }; delete v[key]; return { ...s, verification: v }; };
const setShape = (key, value) => (s) => ({ ...s, run_shape: { ...s.run_shape, [key]: value } });
const cases = [
['no report folder', null],
['a stamp of another commit', { head: 'c'.repeat(40) }],
['a stamp of another merge base', { stamp: (s) => ({ ...s, revision: { ...s.revision, merge_base: 'd'.repeat(40) } }) }],
['a commit scan, not a changes scan', { stamp: (s) => ({ ...s, mode: 'commit' }) }],
['verification unverified', { stamp: setV('status', 'unverified') }],
['fewer researchers returned than dispatched', { stamp: setV('researchers_returned', 2) }],
['researchers_returned missing', { stamp: dropV('researchers_returned') }],
['researchers_dispatched missing', { stamp: dropV('researchers_dispatched') }],
['the scan saw an empty diff', { stamp: setShape('empty_diff', true) }],
['the scan saw an empty scope', { stamp: setShape('empty_scope', true) }],
['no JSONL', { jsonl: null }],
['a JSONL line that does not parse', { jsonl: `${JSON.stringify(record())}\n{"title": \n` }],
['a record with an unknown confidence', { records: [record({ confidence: 'certain' })] }],
['a record with no title', { records: [record({ title: '' })] }],
];
// The CLI's channels, once: exit 2, nothing on stdout, the reason on stderr.
const cli = run(['convert', '--report-dir', reportDir(), '--base', BASE, '--head', HEAD]);
assert.equal(cli.status, 2, `CLI: exit 2, got ${cli.status}: ${cli.stdout}${cli.stderr}`);
assert.equal(cli.stdout, '', 'CLI: nothing on stdout');
assert.match(cli.stderr, /not measured/, `CLI: the reason on stderr says not measured: ${cli.stderr}`);
for (const [name, opts] of cases) {
const dir = reportDir();
if (opts !== null) writeReport(dir, { head: HEAD, base: BASE, ...opts });
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD });
assert.equal(r.exit, 2, `${name}: exit 2, got ${r.exit}: ${JSON.stringify(r)}`);
assert.equal(r.payload, undefined, `${name}: no payload`);
assert.match(r.reason, /not measured/, `${name}: the reason says not measured: ${r.reason}`);
}
});
/** The convert result of one report folder: [name, exit, no payload, the reason says not measured]. */
const convertRow = (name, r) => [name, r.exit, r.payload === undefined, /not measured/.test(r.reason ?? '')];
/** convertReport on one fresh report folder per case, as convertRow gives it. */
function convertEach(cases) {
return cases.map(([name, opts]) => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, ...opts });
return convertRow(name, convertReport({ reportDir: dir, base: BASE, head: HEAD }));
});
}
const notMeasuredEach = (cases) => cases.map(([name]) => [name, 2, true, true]);
test('[CNV] decision 224: a JSONL that does not hold exactly the stamp\'s findings.total records, or a total that is not a non-negative integer, is not measured', () => {
const setTotal = (v) => (s) => ({ ...s, findings: { ...s.findings, total: v } });
const cases = [
['fewer records than the total', { records: [record(), MEDIUM], stamp: setTotal(3) }],
['more records than the total', { records: [record(), MEDIUM], stamp: setTotal(1) }],
['an empty JSONL, total 2', { records: [record(), MEDIUM], jsonl: '', stamp: setTotal(2) }],
['findings.total absent', { stamp: (s) => { const f = { ...s.findings }; delete f.total; return { ...s, findings: f }; } }],
['findings absent', { stamp: (s) => { const t = { ...s }; delete t.findings; return t; } }],
['findings.total 1.5', { stamp: setTotal(1.5) }],
['findings.total "1"', { stamp: setTotal('1') }],
['findings.total null', { stamp: setTotal(null) }],
['findings.total -1, no records', { records: [], stamp: setTotal(-1) }],
];
// Control: the records and the total agree.
assert.deepEqual(convertEach([['two records, total 2', { records: [record(), MEDIUM] }]]), [['two records, total 2', 0, false, false]]);
assert.deepEqual(convertEach(cases), notMeasuredEach(cases));
});
test('[CNV] decision 224: a stamp whose scope is not [] (a scan limited by --scope, or no scope recorded) is not measured', () => {
const setScope = (v) => (s) => ({ ...s, scope: v });
const cases = [
['scope ["bin"]', { stamp: setScope(['bin']) }],
['scope absent', { stamp: (s) => { const t = { ...s }; delete t.scope; return t; } }],
['scope "bin"', { stamp: setScope('bin') }],
['scope {}', { stamp: setScope({}) }],
['scope null', { stamp: setScope(null) }],
];
// Control: an unscoped scan (Claude Security 0.12.0 writes scope: []).
assert.deepEqual(convertEach([['scope []', { stamp: setScope([]) }]]), [['scope []', 0, false, false]]);
assert.deepEqual(convertEach(cases), notMeasuredEach(cases));
});
test('[CNV] the newest matching folder is judged: an unverified newest scan is not measured, never a fallback to an older verified one', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, ts: '20261108-100000', records: [record({ title: 'older verified scan' })] });
const alone = convertReport({ reportDir: dir, base: BASE, head: HEAD });
assert.equal(alone.exit, 0, `Control: the older scan alone is read: ${alone.reason}`);
writeReport(dir, { head: HEAD, base: BASE, ts: '20261108-110000', stamp: (s) => ({ ...s, verification: { ...s.verification, status: 'unverified' } }) });
const r = run(['convert', '--report-dir', dir, '--base', BASE, '--head', HEAD]);
assert.equal(r.status, 2, `exit 2, got ${r.status}: ${r.stdout}${r.stderr}`);
assert.equal(r.stdout, '', 'nothing on stdout');
assert.match(r.stderr, /CLAUDE-SECURITY-20261108-110000\/.*not "verified"/, r.stderr);
});
test('[CNV] a folder with two stamps is passed over: an older one-stamp folder is read; alone, it is not measured', () => {
const stampName = `CLAUDE-SECURITY-REVISION-${HEAD.slice(0, 12)}.json`;
const twoStamps = (dir) => {
const folder = writeReport(dir, { head: HEAD, base: BASE, ts: '20261108-110000', records: [record({ title: 'two-stamp scan' })] });
copyFileSync(join(folder, stampName), join(folder, `CLAUDE-SECURITY-REVISION-${HEAD.slice(0, 12)}-dirty.json`));
};
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE, ts: '20261108-100000', records: [record({ title: 'one-stamp scan' })] });
twoStamps(dir);
const r = convertReport({ reportDir: dir, base: BASE, head: HEAD });
assert.equal(r.exit, 0, r.reason);
assert.deepEqual(r.payload.findings.map((f) => f.title), ['one-stamp scan'], 'the two-stamp folder is passed over');
const lone = reportDir();
twoStamps(lone);
const n = run(['convert', '--report-dir', lone, '--base', BASE, '--head', HEAD]);
assert.equal(n.status, 2, `exit 2, got ${n.status}: ${n.stdout}${n.stderr}`);
assert.equal(n.stdout, '', 'nothing on stdout');
assert.match(n.stderr, /not measured/, n.stderr);
});
test('[CNV] a symlinked stamp, JSONL or report folder is refused: not measured', () => {
const stampName = `CLAUDE-SECURITY-REVISION-${HEAD.slice(0, 12)}.json`;
const kinds = ['stamp', 'JSONL', 'folder'];
/** A report dir whose `what` is a symlink to a regular, valid one. */
const linked = (what) => {
const dir = reportDir();
if (what === 'folder') {
const elsewhere = reportDir();
const real = writeReport(elsewhere, { head: HEAD, base: BASE });
const followed = convertReport({ reportDir: elsewhere, base: BASE, head: HEAD });
assert.equal(followed.exit, 0, `Control: the folder the link names is a valid report: ${followed.reason}`);
symlinkSync(real, join(dir, basename(real)));
return dir;
}
const folder = writeReport(dir, { head: HEAD, base: BASE });
const name = what === 'stamp' ? stampName : 'CLAUDE-SECURITY-RESULTS.jsonl';
renameSync(join(folder, name), join(folder, `${what}.real`));
symlinkSync(`${what}.real`, join(folder, name));
return dir;
};
const got = kinds.map((what) => convertRow(what, convertReport({ reportDir: linked(what), base: BASE, head: HEAD })));
assert.deepEqual(got, kinds.map((what) => [what, 2, true, true]));
});
test('[CNV] a malformed or absent scan_prefix is not measured', () => {
const setPrefix = (v) => (s) => ({ ...s, scan_prefix: v });
const cases = [
['scan_prefix absent', { stamp: (s) => { const t = { ...s }; delete t.scan_prefix; return t; } }],
['scan_prefix null', { stamp: setPrefix(null) }],
['scan_prefix 7', { stamp: setPrefix(7) }],
['scan_prefix "../"', { stamp: setPrefix('../') }],
['scan_prefix "src/../../"', { stamp: setPrefix('src/../../') }],
['scan_prefix "/abs/"', { stamp: setPrefix('/abs/') }],
['scan_prefix "src" (no trailing slash)', { stamp: setPrefix('src') }],
['scan_prefix "./"', { stamp: setPrefix('./') }],
['scan_prefix "src//"', { stamp: setPrefix('src//') }],
['scan_prefix with a backslash', { stamp: setPrefix('src\\lib/') }],
];
// Control: an empty prefix and a plain one are read.
assert.deepEqual(convertEach([['scan_prefix ""', { stamp: setPrefix('') }], ['scan_prefix "src/"', { stamp: setPrefix('src/') }]]),
[['scan_prefix ""', 0, false, false], ['scan_prefix "src/"', 0, false, false]]);
assert.deepEqual(convertEach(cases), notMeasuredEach(cases));
});
test('[CNV] a stamp that does not record whether the diff or the scope was empty is not measured', () => {
const dropShape = (key) => (s) => { const sh = { ...s.run_shape }; delete sh[key]; return { ...s, run_shape: sh }; };
const setShape = (key, value) => (s) => ({ ...s, run_shape: { ...s.run_shape, [key]: value } });
const cases = [
['run_shape.empty_diff absent', { stamp: dropShape('empty_diff') }],
['run_shape.empty_scope absent', { stamp: dropShape('empty_scope') }],
['run_shape absent', { stamp: (s) => { const t = { ...s }; delete t.run_shape; return t; } }],
['run_shape.empty_diff "false"', { stamp: setShape('empty_diff', 'false') }],
['run_shape.empty_scope null', { stamp: setShape('empty_scope', null) }],
];
assert.deepEqual(convertEach(cases), notMeasuredEach(cases));
});
test('[CNV] an empty scope list is exit 1 (the review scope is unknown), as engine-findings.mjs', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE });
const scope = join(dir, 'scope.txt');
writeFileSync(scope, '\n');
const r = run(['convert', '--report-dir', dir, '--base', BASE, '--head', HEAD, '--scope', scope]);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.equal(r.stdout, '');
});
test('[CNV] bad usage is exit 2', () => {
const dir = reportDir();
writeReport(dir, { head: HEAD, base: BASE });
for (const [name, args] of [
['--head missing', ['convert', '--report-dir', dir, '--base', BASE]],
['--base not a full sha', ['convert', '--report-dir', dir, '--base', 'main', '--head', HEAD]],
['--report-dir missing', ['convert', '--base', BASE, '--head', HEAD]],
['a scope file that does not exist', ['convert', '--report-dir', dir, '--base', BASE, '--head', HEAD, '--scope', join(dir, 'none.txt')]],
]) {
const r = run(args);
assert.equal(r.status, 2, `${name}: exit 2, got ${r.status}: ${r.stdout}${r.stderr}`);
assert.equal(r.stdout, '', `${name}: nothing on stdout`);
}
});
// ---- ENGINES --------------------------------------------------------------------
test('[ENG3] ENGINES has claude-security: reviewer claude-security, the engines\' key, its own brief_ref', () => {
const s = ENGINES['claude-security'];
assert.ok(s, 'ENGINES has claude-security');
assert.equal(s.reviewer, 'claude-security');
assert.equal(s.rule_key, 'SECURITY_REVIEW_VULNERABILITY');
assert.equal(typeof s.brief_ref, 'string');
assert.notEqual(s.brief_ref, ENGINES['security-review'].brief_ref, 'its own brief_ref');
assert.match(s.brief_ref, /Claude Security/);
});
test('[ENG3] the action written for a finding without a fix names the engine that reported it', () => {
const bare = [{ file: 'src/run.mjs', line: 3, summary: 's' }];
const action = (engine) => {
const r = toReviewerPayload({ engine, findings: bare });
assert.equal(r.ok, true, `${engine}: ${r.errors.join('; ')}`);
return r.payload.findings[0].recommended_action;
};
assert.match(action('code-review'), /\/code-review reported/);
assert.match(action('security-review'), /\/security-review reported/);
assert.match(action('claude-security'), /Claude Security reported/);
assert.doesNotMatch(action('claude-security'), /\/code-review/, 'another engine is never called /code-review');
});
// ---- no engine: not measured, never ALLOW -----------------------------------------
// Copied from tests/lib/verdict-gate.test.mjs:29-74 (review), never imported. Changed:
// allow_blocked_by is the one argument.
function review(allowBlockedBy) {
const json = JSON.stringify({ verdict: 'WARN', counts: { BLOCKER: 0, MAJOR: 0, MINOR: 0, SUGGESTION: 0 }, allow_blocked_by: allowBlockedBy, findings: [] }, null, 2);
return `---
type: trekreview
review_version: "1.1"
task: "t"
slug: s
project_dir: .claude/projects/2026-11-08-s/
brief_path: .claude/projects/2026-11-08-s/brief.md
scope_sha_end: abc
reviewed_files_count: 1
verdict: WARN
findings: []
---
# Review: t
## Executive Summary
x
## Coverage
| File | Treatment | Reason |
|------|-----------|--------|
| a.mjs | deep-review | default |
## Remediation Summary
x
\`\`\`json
${json}
\`\`\`
`;
}
test('[NOE] no engine chosen: the expected reviewer security-engine never reports, so the review is not measured and the verdict gate exits 2, never ALLOW', () => {
assert.equal(NO_ENGINE_REVIEWER, 'security-engine');
const expected = ['brief-conformance-reviewer', 'code-correctness-reviewer', ENGINES['code-review'].reviewer, NO_ENGINE_REVIEWER];
const c = runContract([
{ reviewer: 'brief-conformance-reviewer', findings: [] },
{ reviewer: 'code-correctness-reviewer', findings: [] },
{ reviewer: ENGINES['code-review'].reviewer, findings: [] },
], { expectedReviewers: expected });
assert.deepEqual(c.missing_reviewers, ['security-engine']);
assert.ok(c.allow_blocked_by.includes('missing-reviewer:security-engine'), JSON.stringify(c.allow_blocked_by));
assert.notEqual(c.verdict, 'ALLOW');
const d = decideVerdict(review(c.allow_blocked_by));
assert.equal(d.exit, 2, d.reason);
assert.match(d.reason, /missing-reviewer:security-engine/);
});