voyage/tests/scripts/trekreview-armb-workflow.test.mjs
2026-10-12 18:16:07 +02:00

323 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// tests/scripts/trekreview-armb-workflow.test.mjs
// The opt-in Workflow substrate for /trekreview Phase 5–6, run for real with
// stubbed Workflow globals (agent/parallel/phase/log). Ordre 5: a reviewer or
// engine that did not report forbids ALLOW, and the verdict's floor is
// recomputed from the findings the coordinator returns.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { illegalBriefLowering } from '../../lib/review/coordinator-contract.mjs';
import { RULE_CATALOGUE } from '../../lib/review/rule-catalogue.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const SRC = readFileSync(join(ROOT, 'scripts', 'trekreview-armB.workflow.mjs'), 'utf-8')
.replace('export const meta', 'const meta');
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
const run = new AsyncFunction('args', 'agent', 'parallel', 'phase', 'log', SRC);
const BASE = { briefPath: 'b.md', diffPath: 'd.diff', triage: '' };
const engine = (reviewer, findings = []) => ({ reviewer, findings });
function stubs({ reviewers = [], coordinator = null } = {}) {
let i = 0;
const calls = { coordinatorPrompt: null, coordinatorOpts: null };
return {
calls,
agent: async (prompt, opts) => {
if (opts.label === 'coordinator') {
calls.coordinatorPrompt = prompt;
calls.coordinatorOpts = opts;
return coordinator;
}
return reviewers[i++] ?? null;
},
parallel: async (thunks) => Promise.all(thunks.map((t) => t())),
phase: () => {},
log: () => {},
};
}
async function exec(args, s) {
return run(args, s.agent, s.parallel, s.phase, s.log);
}
test('armB — both reviewers and both engines clean → ALLOW (known-positive control)', async () => {
const s = stubs({ reviewers: [{ reviewer: 'c', findings: [] }, { reviewer: 'k', findings: [] }] });
const r = await exec({ ...BASE, enginePayloads: [engine('claude-code-review'), engine('claude-security-review')] }, s);
assert.equal(r.verdict, 'ALLOW');
assert.deepEqual(r.missing_engines, []);
});
test('armB — engines the caller never mentioned are missing → not ALLOW', async () => {
const s = stubs({ reviewers: [{ reviewer: 'c', findings: [] }, { reviewer: 'k', findings: [] }] });
const r = await exec({ ...BASE }, s);
assert.equal(r.verdict, 'WARN');
assert.deepEqual(r.missing_engines, ['claude-code-review', 'claude-security-review']);
});
test('armB — a reviewer that returned nothing → not ALLOW', async () => {
const s = stubs({ reviewers: [null, { reviewer: 'k', findings: [] }] });
const r = await exec({ ...BASE, enginePayloads: [engine('claude-code-review'), engine('claude-security-review')] }, s);
assert.equal(r.verdict, 'WARN');
});
test('armB — a coordinator ALLOW over a BLOCKER finding is recomputed to BLOCK', async () => {
const f = { severity: 'BLOCKER', rule_key: 'UNIMPLEMENTED_CRITERION', file: 'a.mjs', line: 1, title: 't', detail: 'd' };
const s = stubs({
reviewers: [{ reviewer: 'c', findings: [f] }, { reviewer: 'k', findings: [] }],
coordinator: { verdict: 'ALLOW', findings: [f] },
});
const r = await exec({ ...BASE, enginePayloads: [engine('claude-code-review'), engine('claude-security-review')] }, s);
assert.equal(r.verdict, 'BLOCK');
});
test('armB — quick mode expects only the /code-review engine', async () => {
const s = stubs({ reviewers: [{ reviewer: 'k', findings: [] }] });
const r = await exec({ ...BASE, quick: true, enginePayloads: [engine('claude-code-review')] }, s);
assert.equal(r.verdict, 'ALLOW');
assert.deepEqual(r.missing_engines, []);
});
// ---- Step 9: the brief-conflict rule on the Workflow path ---------------------------
//
// Each case stubs the reviewers' and engines' payloads (so `merged` holds the
// input), the coordinator's returned findings and its verdict, then asserts the
// workflow's verdict. `merged` is read from the prompt the coordinator stub
// receives. For every case the test states the value of `illegalBriefLowering`
// on the subject finding as returned, with `engine` set from its counterpart's
// source (removed when the counterpart is a reviewer finding or absent), and
// checks the floor raised a finding exactly where that value is true — plus
// case 7, where a false `engine` label alone raises.
const RANK = ['BLOCKER', 'MAJOR', 'MINOR', 'SUGGESTION'];
const rank = (s) => (RANK.indexOf(s) === -1 ? RANK.length : RANK.indexOf(s));
const MARK = 'Non-Goal: no new runtime dependency';
const FILE = 'src/a.mjs';
const LINE = 10;
const F = (rule_key, severity, extra = {}) => ({ severity, rule_key, file: FILE, line: LINE, title: 't', detail: 'd', ...extra });
const PROMPT_JSON_MARK = '=== PRE-DEDUPED FINDINGS (JSON) ===';
const triplet = (f) => `${f.file}\u0000${f.line}\u0000${f.rule_key}`;
async function floorCase({ reviewerFindings = [], codeFindings = [], securityFindings = [], quick = false, returned, verdict }) {
const before = structuredClone(returned);
const coordinator = { verdict, findings: structuredClone(returned) };
const reviewers = quick
? [{ reviewer: 'code-correctness', findings: structuredClone(reviewerFindings) }]
: [{ reviewer: 'brief-conformance', findings: structuredClone(reviewerFindings) }, { reviewer: 'code-correctness', findings: [] }];
const enginePayloads = quick
? [engine('claude-code-review', structuredClone(codeFindings))]
: [engine('claude-code-review', structuredClone(codeFindings)), engine('claude-security-review', structuredClone(securityFindings))];
const s = stubs({ reviewers, coordinator });
const r = await exec({ ...BASE, quick, enginePayloads }, s);
const prompt = s.calls.coordinatorPrompt;
assert.ok(typeof prompt === 'string', 'the coordinator ran');
const merged = JSON.parse(prompt.slice(prompt.indexOf(PROMPT_JSON_MARK) + PROMPT_JSON_MARK.length));
const raised = r.findings.some((f, i) => rank(f.severity) < rank(before[i].severity));
return { r, merged, before, raised, s };
}
// The subject (index 0 of `returned`) with `engine` from its counterpart's source.
function subjectIllegal(before, merged, idx = 0) {
const subject = { ...before[idx] };
const counterpart = merged.find((m) => triplet(m) === triplet(subject));
if (counterpart && counterpart.engine) subject.engine = counterpart.engine;
else delete subject.engine;
return illegalBriefLowering(subject);
}
function checkRaise({ before, merged, raised }, statedIllegal, extraRaise = false) {
assert.equal(subjectIllegal(before, merged), statedIllegal, 'illegalBriefLowering on the subject');
assert.equal(raised, statedIllegal || extraRaise, 'the floor raised a finding exactly where the shape is illegal');
}
test('armB brief rule (1) — an unlabelled SECURITY_REVIEW_VULNERABILITY returned at MINOR over a /security-review BLOCKER → BLOCK', async () => {
const c = await floorCase({
securityFindings: [F('SECURITY_REVIEW_VULNERABILITY', 'BLOCKER')],
returned: [F('SECURITY_REVIEW_VULNERABILITY', 'MINOR')],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'BLOCK');
assert.equal(c.r.findings[0].engine, 'security-review');
checkRaise(c, true);
});
test('armB brief rule (2) — an unlabelled CODE_REVIEW_DEFECT returned at MINOR over a /code-review MAJOR with ALLOW → WARN', async () => {
const c = await floorCase({
codeFindings: [F('CODE_REVIEW_DEFECT', 'MAJOR')],
returned: [F('CODE_REVIEW_DEFECT', 'MINOR')],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'WARN');
checkRaise(c, true);
});
for (const withEngine of [true, false]) {
test(`armB brief rule (3) — the legal lowering keeps ALLOW (returned ${withEngine ? 'with' : 'without'} engine) and carries engine code-review`, async () => {
const lowered = F('CODE_REVIEW_DEFECT', 'MINOR', { contradicts_brief: MARK, brief_lowered_from: 'MAJOR' });
if (withEngine) lowered.engine = 'code-review';
const c = await floorCase({
codeFindings: [F('CODE_REVIEW_DEFECT', 'MAJOR')],
returned: [lowered],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'ALLOW');
assert.equal(c.r.findings[0].engine, 'code-review');
assert.equal(c.r.findings[0].severity, 'MINOR');
checkRaise(c, false);
});
}
test('armB brief rule (4) — the legal shape over a reviewer CODE_REVIEW_DEFECT asserted BLOCKER (relabelled engine) → BLOCK, no engine', async () => {
const c = await floorCase({
reviewerFindings: [F('CODE_REVIEW_DEFECT', 'BLOCKER')],
returned: [F('CODE_REVIEW_DEFECT', 'MINOR', { engine: 'code-review', contradicts_brief: MARK, brief_lowered_from: 'MAJOR' })],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'BLOCK');
assert.equal(c.r.findings[0].engine, undefined);
checkRaise(c, true);
});
test('armB brief rule (5) — a reviewer finding that carries engine code-review is not an engine finding: the source decides → WARN', async () => {
const c = await floorCase({
reviewerFindings: [F('CODE_REVIEW_DEFECT', 'MAJOR', { engine: 'code-review' })],
returned: [F('CODE_REVIEW_DEFECT', 'MINOR', { engine: 'code-review', contradicts_brief: MARK, brief_lowered_from: 'MAJOR' })],
verdict: 'ALLOW',
});
assert.equal(c.merged[0].engine, undefined, 'a reviewer engine field never reaches merged');
assert.equal(c.r.verdict, 'WARN');
checkRaise(c, true);
});
test('armB brief rule (6) — a marked reviewer MISSING_TEST asserted BLOCKER, returned at MAJOR with WARN → BLOCK', async () => {
const c = await floorCase({
reviewerFindings: [F('MISSING_TEST', 'BLOCKER')],
returned: [F('MISSING_TEST', 'MAJOR', { contradicts_brief: MARK })],
verdict: 'WARN',
});
assert.equal(c.r.verdict, 'BLOCK');
checkRaise(c, true);
});
test('armB brief rule (7) — a reviewer MISSING_TEST asserted BLOCKER, returned at MAJOR with only engine security-review → BLOCK, no engine', async () => {
const c = await floorCase({
reviewerFindings: [F('MISSING_TEST', 'BLOCKER')],
returned: [F('MISSING_TEST', 'MAJOR', { engine: 'security-review' })],
verdict: 'WARN',
});
assert.equal(c.r.verdict, 'BLOCK');
assert.equal(c.r.findings[0].engine, undefined);
checkRaise(c, false, true);
});
test('armB brief rule (8) — a stray brief_lowered_from on a reviewer finding asserted BLOCKER, returned at MAJOR with WARN → BLOCK', async () => {
const c = await floorCase({
reviewerFindings: [F('MISSING_TEST', 'BLOCKER')],
returned: [F('MISSING_TEST', 'MAJOR', { brief_lowered_from: 'MAJOR' })],
verdict: 'WARN',
});
assert.equal(c.r.verdict, 'BLOCK');
checkRaise(c, true);
});
test('armB brief rule (9) — a marked finding with no counterpart and an unknown rule_key, returned at MINOR with ALLOW → BLOCK', async () => {
const other = { severity: 'MINOR', rule_key: 'MISSING_ERROR_HANDLING', file: 'src/b.mjs', line: 3, title: 't', detail: 'd' };
const c = await floorCase({
reviewerFindings: [other],
returned: [{ severity: 'MINOR', rule_key: 'NOT_IN_THE_CATALOGUE', file: 'src/ghost.mjs', line: 7, title: 't', detail: 'd', contradicts_brief: MARK }, other],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'BLOCK');
checkRaise(c, true);
});
test('armB brief rule (10) — a marked BLOCKER-tier /code-review finding returned at MINOR → BLOCK', async () => {
const c = await floorCase({
codeFindings: [F('SECURITY_INJECTION', 'BLOCKER')],
returned: [F('SECURITY_INJECTION', 'MINOR', { engine: 'code-review', contradicts_brief: MARK })],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'BLOCK');
checkRaise(c, true);
});
test('armB brief rule (11) — dedup keeps the /code-review MAJOR over a first-seen reviewer MINOR on the same triplet → WARN', async () => {
const c = await floorCase({
reviewerFindings: [F('CODE_REVIEW_DEFECT', 'MINOR')],
codeFindings: [F('CODE_REVIEW_DEFECT', 'MAJOR')],
returned: [F('CODE_REVIEW_DEFECT', 'MINOR')],
verdict: 'ALLOW',
});
assert.equal(c.merged.length, 1);
assert.equal(c.merged[0].severity, 'MAJOR');
assert.equal(c.merged[0].engine, 'code-review');
assert.equal(c.r.deduped_count, 1);
assert.equal(c.r.raw_finding_count, 2);
assert.equal(c.r.verdict, 'WARN');
checkRaise(c, true);
});
test('armB brief rule (12) — an unlabelled reviewer MISSING_TEST asserted BLOCKER, returned at MAJOR (catalogue reset) with WARN stays WARN', async () => {
const c = await floorCase({
reviewerFindings: [F('MISSING_TEST', 'BLOCKER')],
returned: [F('MISSING_TEST', 'MAJOR')],
verdict: 'WARN',
});
assert.equal(c.r.verdict, 'WARN');
checkRaise(c, false);
});
test('armB brief rule (13) — a marked MINOR-tier engine finding at its tier (CODE_REVIEW_IMPROVEMENT) keeps ALLOW', async () => {
const c = await floorCase({
codeFindings: [F('CODE_REVIEW_IMPROVEMENT', 'MINOR')],
returned: [F('CODE_REVIEW_IMPROVEMENT', 'MINOR', { engine: 'code-review', contradicts_brief: MARK })],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'ALLOW');
assert.equal(c.r.findings[0].engine, 'code-review');
checkRaise(c, false);
});
test('armB brief rule (14) — an unlabelled reviewer MISSING_TEST at MINOR (quick mode) keeps the coordinator verdict', async () => {
const c = await floorCase({
quick: true,
reviewerFindings: [F('MISSING_TEST', 'MINOR')],
returned: [F('MISSING_TEST', 'MINOR')],
verdict: 'ALLOW',
});
assert.equal(c.r.verdict, 'ALLOW');
checkRaise(c, false);
});
test('armB brief rule — RULE_TIERS in the script equals the catalogue (same keys, same tiers)', () => {
const start = SRC.indexOf('const RULE_TIERS = {');
assert.ok(start !== -1, 'the script carries a RULE_TIERS literal block');
const block = SRC.slice(start, SRC.indexOf('};', start));
const tiers = Object.fromEntries([...block.matchAll(/([A-Z_]+):\s*'([A-Z]+)'/g)].map((m) => [m[1], m[2]]));
const catalogue = Object.fromEntries(RULE_CATALOGUE.map((r) => [r.rule_key, r.severity]));
assert.deepEqual(tiers, catalogue);
});
test('armB brief rule — VERDICT_SCHEMA declares engine, contradicts_brief and brief_lowered_from; the prompt states the rule', async () => {
const c = await floorCase({
codeFindings: [F('CODE_REVIEW_DEFECT', 'MAJOR')],
returned: [F('CODE_REVIEW_DEFECT', 'MAJOR')],
verdict: 'WARN',
});
const props = c.s.calls.coordinatorOpts.schema.properties.findings.items.properties;
assert.deepEqual(props.engine, { type: 'string', enum: ['code-review', 'security-review'] });
assert.deepEqual(props.contradicts_brief, { type: 'string' });
assert.deepEqual(props.brief_lowered_from, { type: 'string', enum: ['MAJOR'] });
const prompt = c.s.calls.coordinatorPrompt;
assert.ok(prompt.includes('contradicts_brief'));
assert.ok(prompt.includes('brief_lowered_from'));
assert.ok(prompt.includes('## Non-Goals'));
assert.ok(prompt.includes('## Constraints'));
assert.ok(prompt.includes('## Preferences'));
assert.ok(prompt.includes('## Open Questions / Assumptions'));
assert.ok(/only an engine finding/i.test(prompt), 'engine findings only');
assert.ok(/kept and marked, never removed/i.test(prompt));
});