116 lines
6.9 KiB
JavaScript
116 lines
6.9 KiB
JavaScript
// 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<n> 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<n> 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)}`);
|
|
}
|
|
});
|