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