Ordre 5 (wave 1, P11 + P5), review_version 1.1. - K7: `verdict: ALLOW | WARN | BLOCK` is required in review.md (strict error, soft warning for 1.0 reviews). New CLI lib/review/verdict-gate.mjs: 0 = ALLOW/WARN, 1 = BLOCK, 2 = could not decide (never green); a verdict greener than the file's own BLOCKERs or JSON block is exit 2. Realpath main guard (the old file:// guard exits 0 under a symlink - measured). - No finding is struck for length: succinctness trims only MINOR/SUGGESTION text (catalogue tier), keeps BLOCKER/MAJOR whole, counts trims; the coordinator lists what it declined to judge. - Claude Code's /code-review and /security-review run as the engine via the Skill tool (never --fix/--comment/--post/ultra); lib/review/engine-findings.mjs turns their findings into a reviewer payload; two catalogue keys (CODE_REVIEW_DEFECT, SECURITY_REVIEW_VULNERABILITY), catalogue 12 -> 14. An engine that did not run forbids ALLOW. - Every BLOCKER is written as a numbered `### Step R<n>` with its source finding (strict error REVIEW_MISSING_REMEDIATION_STEP). - P5: lib/review/recurrence.mjs - a finding class in two review.md files (current + git history, no new store) becomes one proposal, routed by the catalogue's memory_route; written only between Voyage markers in .claude/rules/voyage-review-lessons.md under a size budget, CLAUDE.md gets a pointer only; the operator approves every line (Phase 8.7). - Workflow substrate: catalogue enum in step; a missing reviewer or engine forbids ALLOW there too (it returned ALLOW on zero findings before). - Fixtures review-run-A/B said WARN over a BLOCKER; now BLOCK. Borrowed and credited in code: Superpowers (declined-to-judge), ai-native-sdlc (cap only nits; mistake/fix/check), Spec Kit (managed block; converge steps), Pocock (retro routing), Kiro (verdict file + gate; evidence per lesson), Cherny (engine -> brief control order; review -> memory; budget). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
6.1 KiB
JavaScript
128 lines
6.1 KiB
JavaScript
// tests/lib/review-prose-pins.test.mjs
|
|
// Prose pins for the review process (ordre 5, bølge 1: P11 + P5). Counted as
|
|
// doc-pins by lib/util/test-census.mjs. Each pin names the behaviour it guards
|
|
// so a failing pin says what broke, not just which string moved.
|
|
|
|
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 { RULE_KEYS } from '../../lib/review/rule-catalogue.mjs';
|
|
import { UNVERIFIED_REASONS } from '../../lib/review/coordinator-contract.mjs';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const read = (rel) => readFileSync(join(ROOT, rel), 'utf-8');
|
|
|
|
function section(text, heading) {
|
|
const start = text.indexOf(heading);
|
|
assert.ok(start >= 0, `heading missing: ${heading}`);
|
|
const next = text.indexOf('\n## ', start + heading.length);
|
|
return text.slice(start, next === -1 ? undefined : next);
|
|
}
|
|
|
|
const CMD = read('commands/trekreview.md');
|
|
const COORD = read('agents/review-coordinator.md');
|
|
|
|
// ---- P11: Claude Code's review as the engine --------------------------------
|
|
|
|
test('trekreview — frontmatter allows the Skill tool (the engine is started through it)', () => {
|
|
const fm = CMD.slice(0, CMD.indexOf('\n---', 4));
|
|
assert.match(fm, /allowed-tools:.*\bSkill\b/);
|
|
});
|
|
|
|
test('trekreview — Phase 4.7 starts /code-review and /security-review through the Skill tool', () => {
|
|
const p = section(CMD, '## Phase 4.7');
|
|
assert.ok(p.includes('Skill("code-review"'), 'code-review via Skill');
|
|
assert.ok(p.includes('Skill("security-review")'), 'security-review via Skill');
|
|
assert.ok(p.includes('lib/review/engine-findings.mjs'), 'engine findings go through the adapter');
|
|
});
|
|
|
|
test('trekreview — the engine never edits code, posts or uploads (no --fix/--comment/--post/ultra)', () => {
|
|
const p = section(CMD, '## Phase 4.7');
|
|
assert.match(p, /\*\*Never\*\* pass `--fix`, `--comment`, `--post` or `ultra`/);
|
|
const call = p.match(/Skill\("code-review", args: "([^"]*)"\)/);
|
|
assert.ok(call, 'the code-review call is written out');
|
|
for (const banned of ['--fix', '--comment', '--post', 'ultra']) {
|
|
assert.ok(!call[1].includes(banned), `the engine call must not carry ${banned}`);
|
|
}
|
|
});
|
|
|
|
test('trekreview — an engine that did not run forbids ALLOW (fail-closed), and engine payloads stay out of the reviewers', () => {
|
|
const p = section(CMD, '## Phase 4.7');
|
|
assert.match(p, /forbids `ALLOW`/);
|
|
assert.match(p, /NOT shown to the Phase 5\s+reviewers/);
|
|
});
|
|
|
|
test('trekreview — the engine claims cite the live docs they were verified in', () => {
|
|
const p = section(CMD, '## Phase 4.7');
|
|
assert.ok(p.includes('v2.1.246'), 'model-invocation floor for /code-review');
|
|
assert.ok(p.includes("origin's"), '/security-review base is origin default branch');
|
|
});
|
|
|
|
// ---- P11: no finding struck for length; declined-to-judge ----------------------
|
|
|
|
test('coordinator — succinctness never removes a finding; only MINOR/SUGGESTION text is trimmed', () => {
|
|
assert.match(COORD, /\| Succinctness \|[^\n]*never removed/);
|
|
assert.ok(!UNVERIFIED_REASONS.includes('succinctness:detail'), 'length is no longer an unverified reason');
|
|
assert.ok(!UNVERIFIED_REASONS.includes('succinctness:title'));
|
|
});
|
|
|
|
test('coordinator — writes ## Declined to Judge with the trim count', () => {
|
|
assert.ok(COORD.includes('`## Declined to Judge` — REQUIRED'));
|
|
assert.ok(COORD.includes('Trimmed: N'));
|
|
});
|
|
|
|
// ---- K7: the verdict in the file, and the gate -------------------------------
|
|
|
|
test('coordinator — review_version 1.1 frontmatter carries verdict', () => {
|
|
assert.ok(COORD.includes('review_version: "1.1"'));
|
|
assert.match(COORD, /\nverdict: \{BLOCK \| WARN \| ALLOW\}\n/);
|
|
});
|
|
|
|
test('trekreview — Phase 8 decides with lib/review/verdict-gate.mjs and calls exit 2 not green', () => {
|
|
const p = section(CMD, '## Phase 8 —');
|
|
assert.ok(p.includes('lib/review/verdict-gate.mjs'));
|
|
assert.match(p, /never\s+green/);
|
|
});
|
|
|
|
test('trekreview — a BLOCKER is written as a numbered Step R<n> with Source finding (Handover 6)', () => {
|
|
assert.ok(CMD.includes('### Step R<n>'));
|
|
assert.ok(COORD.includes('**Source finding:**'));
|
|
const h6 = section(read('docs/HANDOVER-CONTRACTS.md'), '## Handover 6');
|
|
assert.ok(h6.includes('Remediation Steps'), 'Handover 6 documents the remediation steps');
|
|
assert.match(h6, /\| `verdict` \| string \| yes/, 'Handover 6 lists verdict as required');
|
|
assert.ok(h6.includes('verdict-gate.mjs'), 'Handover 6 names the gate');
|
|
});
|
|
|
|
// ---- P5: project memory --------------------------------------------------------
|
|
|
|
test('trekreview — Phase 8.7 asks the operator per line (AskUserQuestion) before recurrence.mjs apply', () => {
|
|
const p = section(CMD, '## Phase 8.7');
|
|
const ask = p.indexOf('AskUserQuestion');
|
|
const apply = p.indexOf('recurrence.mjs apply');
|
|
assert.ok(ask >= 0 && apply > ask, 'approval comes before the write');
|
|
assert.match(p, /over-budget/);
|
|
assert.match(p, /belong in a skill/);
|
|
});
|
|
|
|
test('trekreview — the write-outside-project_dir exception names only the marked block and the pointer', () => {
|
|
const rules = section(CMD, '## Hard rules');
|
|
assert.ok(rules.includes('.claude/rules/voyage-review-lessons.md'));
|
|
assert.ok(rules.includes('only after the operator approved that exact line'));
|
|
});
|
|
|
|
// ---- the Workflow substrate stays in step -----------------------------------------
|
|
|
|
test('armB workflow — its inlined rule-key enum equals the catalogue (no drift)', () => {
|
|
const src = read('scripts/trekreview-armB.workflow.mjs');
|
|
const block = src.slice(src.indexOf('const RULE_KEYS = ['), src.indexOf('];', src.indexOf('const RULE_KEYS = [')));
|
|
const keys = [...block.matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]).sort();
|
|
assert.deepEqual(keys, [...RULE_KEYS].sort());
|
|
});
|
|
|
|
test('armB workflow — a missing reviewer or engine forbids ALLOW on the Workflow path too', () => {
|
|
const src = read('scripts/trekreview-armB.workflow.mjs');
|
|
assert.ok(src.includes("if (verdict === 'ALLOW' && missingReviewers > 0) verdict = 'WARN';"));
|
|
assert.ok(src.includes('enginePayloads'));
|
|
});
|