// tests/lib/review-validator-1-2.test.mjs // Fix round 4.5b2 part B, Step 2 (tag V12): review_version 1.2 validates, and // a 1.1 review still does (N-1 window). Every new 1.2 field is optional; a // malformed one present is a strict error (a soft warning in --soft) with one // of four codes. Each [V12] case fails at its base on an assertion: the // fixtures are new, so each is read only after `assert.ok(existsSync(p))`, // and the base validator knows no 1.2 field, so a malformed one passes there. // The kept behaviour (a 1.1 review validates, unknown keys are tolerated) // sits inside the tagged tests as "Control:" assertions. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { validateReviewContent } from '../../lib/validators/review-validator.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const FIXTURES = 'tests/fixtures/review-version'; /** A fixture's text; the fixture must exist (they are new in this step). */ function fixture(name) { const p = join(ROOT, FIXTURES, name); assert.ok(existsSync(p), `missing fixture ${FIXTURES}/${name}`); return readFileSync(p, 'utf-8'); } const codes = (list) => list.map((i) => i.code); // The ids the 1.2 fixture uses (written down here, independent of the validator). const FIXED_ID = 'e1fa7656c24e23e316f96169884da5f15059beb9'; /** `text` with `from` replaced once; `from` must be in it, or the mutation proves nothing. */ function mutate(text, from, to) { assert.ok(text.includes(from), `mutation source missing from the fixture: ${JSON.stringify(from)}`); return text.replace(from, to); } test('[V12] the 1.2 fixture validates strictly and the 1.1 fixture validates', () => { const v12 = fixture('review-1-2.md'); assert.match(v12, /^review_version: "1\.2"$/m, 'the 1.2 fixture declares review_version "1.2"'); const r = validateReviewContent(v12, { strict: true }); assert.equal(r.valid, true, `the 1.2 fixture is valid in strict mode: ${JSON.stringify(r.errors)}`); assert.deepEqual(r.errors, []); // The 1.2 fields of the fixture are read, not merely tolerated: the same file // with an out-of-range round is refused. const badRound = mutate(v12, '\nround: 1\n', '\nround: 4\n'); const rb = validateReviewContent(badRound, { strict: true }); assert.equal(rb.valid, false, 'round: 4 is outside 1-3 and must be refused at 1.2'); assert.ok(codes(rb.errors).includes('REVIEW_ROUND_MALFORMED'), JSON.stringify(rb.errors)); // Control: a 1.1 review validates as before (N-1 window), strict and soft. const v11 = fixture('review-1-1.md'); assert.match(v11, /^review_version: "1\.1"$/m, 'the 1.1 fixture declares review_version "1.1"'); assert.match(v11, /^verdict: WARN$/m, 'the 1.1 fixture is a WARN review'); const r11 = validateReviewContent(v11, { strict: true }); assert.equal(r11.valid, true, `the 1.1 fixture is valid in strict mode: ${JSON.stringify(r11.errors)}`); assert.equal(validateReviewContent(v11, { strict: false }).valid, true, 'the 1.1 fixture is valid in soft mode'); // Control: at 1.1 the new keys are unknown keys, and unknown keys stay tolerated. const v11Extra = mutate(v11, '\nverdict: WARN\n', '\nverdict: WARN\nround: 9\nreview_policy_sha256: xyz\n'); assert.equal(validateReviewContent(v11Extra, { strict: true }).valid, true, 'a 1.1 review is not held to the 1.2 field shapes'); }); test('[V12] the bad-answers and bad-policy fixtures fail with their codes', () => { const badAnswers = fixture('review-1-2-bad-answers.md'); assert.match(badAnswers, /^\| [0-9a-f]{40} \| maybe \|/m, 'the bad-answers fixture has a row answered "maybe"'); const ra = validateReviewContent(badAnswers, { strict: true }); assert.equal(ra.valid, false, 'an ## Answers row with answer "maybe" is refused in strict mode'); assert.ok(codes(ra.errors).includes('REVIEW_ANSWERS_MALFORMED'), JSON.stringify(ra.errors)); const sa = validateReviewContent(badAnswers, { strict: false }); assert.equal(sa.valid, true, 'soft mode reads it with a warning, not an error'); assert.ok(codes(sa.warnings).includes('REVIEW_ANSWERS_MALFORMED'), JSON.stringify(sa.warnings)); const badPolicy = fixture('review-1-2-bad-policy.md'); assert.match(badPolicy, /^review_policy_sha256: xyz$/m, 'the bad-policy fixture has review_policy_sha256: xyz'); const rp = validateReviewContent(badPolicy, { strict: true }); assert.equal(rp.valid, false, 'review_policy_sha256: xyz is refused in strict mode'); assert.ok(codes(rp.errors).includes('REVIEW_POLICY_MALFORMED'), JSON.stringify(rp.errors)); const sp = validateReviewContent(badPolicy, { strict: false }); assert.equal(sp.valid, true, 'soft mode reads it with a warning, not an error'); assert.ok(codes(sp.warnings).includes('REVIEW_POLICY_MALFORMED'), JSON.stringify(sp.warnings)); }); test('[V12] a `fixed` answer without its R-step fails', () => { const v12 = fixture('review-1-2.md'); assert.match(v12, new RegExp(`^\\| ${FIXED_ID} \\| fixed \\|`, 'm'), 'the 1.2 fixture answers one MAJOR "fixed"'); // Remove the one ### Step R that names the fixed finding. const step = v12.match(/^### Step R1\b[\s\S]*?(?=^## )/m); assert.ok(step && step[0].includes(FIXED_ID), 'the 1.2 fixture has a ### Step R1 naming the fixed finding'); const noStep = v12.replace(step[0], ''); const r = validateReviewContent(noStep, { strict: true }); assert.equal(r.valid, false, 'a MAJOR answered "fixed" without a ### Step R naming it is refused'); const hit = r.errors.find((e) => e.code === 'REVIEW_ANSWERS_MALFORMED' && e.message.includes(FIXED_ID)); assert.ok(hit, `the error names the fixed finding: ${JSON.stringify(r.errors)}`); }); test('[V12] a malformed round, pre-existing or ran field at 1.2 fails with its code', () => { const v12 = fixture('review-1-2.md'); const cases = [ ['round_base not full hex', '\nround_base: null\n', '\nround_base: abc123\n', 'REVIEW_ROUND_MALFORMED'], ['finalized not boolean', '\nfinalized: true\n', '\nfinalized: yes\n', 'REVIEW_ROUND_MALFORMED'], ['pre_existing not boolean', '"pre_existing": true', '"pre_existing": "yes"', 'REVIEW_PRE_EXISTING_MALFORMED'], ['pre_existing_counts not counts', '"pre_existing_counts": {"BLOCKER": 0, "MAJOR": 1, "MINOR": 0, "SUGGESTION": 0}', '"pre_existing_counts": {"MAJOR": "one"}', 'REVIEW_PRE_EXISTING_MALFORMED'], ['ran engine unknown', '"engines": ["code-review", "security-review"]', '"engines": ["code-review", "lint"]', 'REVIEW_ROUND_MALFORMED'], ['review_policy unknown', '\nreview_policy: applied\n', '\nreview_policy: maybe\n', 'REVIEW_POLICY_MALFORMED'], ]; for (const [name, from, to, code] of cases) { const r = validateReviewContent(mutate(v12, from, to), { strict: true }); assert.equal(r.valid, false, `${name}: refused in strict mode`); assert.ok(codes(r.errors).includes(code), `${name}: ${code} expected, got ${JSON.stringify(r.errors)}`); } });