154 lines
8.5 KiB
JavaScript
154 lines
8.5 KiB
JavaScript
// tests/validators/plan-version-1-9.test.mjs
|
|
// plan_version 1.9: each `Files:` entry opens with its backticked path(s), and
|
|
// everything after the first other text is description that is never read.
|
|
// An entry that opens with anything else is refused (an error under --strict,
|
|
// a warning under --soft). Decision 16 (operator 11.10): only a 1.9 plan is
|
|
// refused; a plan below 1.9 is read as before and gets one warning,
|
|
// PLAN_STEP_PATH_UNTICKED, for a path its reader found outside backticks.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import * as schema from '../../lib/parsers/plan-schema.mjs';
|
|
import { validatePlanContent } from '../../lib/validators/plan-validator.mjs';
|
|
import { gatePlan, recordVerdict, planHash } from '../../lib/validators/plan-gate.mjs';
|
|
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
|
|
|
|
const errCodes = (r) => r.errors.map((e) => e.code);
|
|
const warnCodes = (r) => r.warnings.map((w) => w.code);
|
|
const PATH_CODES = ['PLAN_STEP_PATH_NOT_FIRST', 'PLAN_STEP_PATH_NOT_A_PATH'];
|
|
const pathIssues = (list) => list.filter((i) => PATH_CODES.includes(i.code));
|
|
// A 1.9 plan whose first step's `Files:` value is `files` (verbatim).
|
|
const plan19 = (files) => buildPlan({ version: '1.9', steps: [{ filesRaw: files }, { endToEnd: 'bash tests/e2e.sh' }] });
|
|
const BOTH = [{ agent: 'plan-critic', present: true }, { agent: 'scope-guardian', present: true }];
|
|
const passedCritic = (text) => recordVerdict(text, { findings: [], sources: BOTH, dedup_stats: { total_in: 0, total_out: 0 } },
|
|
{ reviewed: planHash(text) });
|
|
|
|
function entryPaths(text) {
|
|
assert.equal(typeof schema.entryPaths, 'function', 'plan-schema exports entryPaths');
|
|
return schema.entryPaths(text);
|
|
}
|
|
|
|
test('1.9: a plan_version 1.9 plan is supported - no PLAN_VERSION_MISMATCH, and the builder plan validates strict', () => {
|
|
const r = validatePlanContent(buildPlan({ version: '1.9' }));
|
|
assert.equal(r.warnings.find((w) => w.code === 'PLAN_VERSION_MISMATCH'), undefined, JSON.stringify(r.warnings));
|
|
assert.deepEqual(r.errors, [], JSON.stringify(r.errors, null, 2));
|
|
assert.equal(r.parsed.planVersion, '1.9');
|
|
});
|
|
|
|
test('1.9: a Files entry without a leading backticked path is an error under --strict and a warning under --soft', () => {
|
|
const text = plan19('src/mod1.mjs - the parser');
|
|
const strict = validatePlanContent(text);
|
|
assert.equal(strict.valid, false);
|
|
const e = strict.errors.find((i) => i.code === 'PLAN_STEP_PATH_NOT_FIRST');
|
|
assert.ok(e, errCodes(strict).join(','));
|
|
assert.match(e.message, /Step 1/);
|
|
assert.match(e.message, /src\/mod1\.mjs - the parser/);
|
|
const soft = validatePlanContent(text, { strict: false });
|
|
assert.ok(warnCodes(soft).includes('PLAN_STEP_PATH_NOT_FIRST'), warnCodes(soft).join(','));
|
|
assert.deepEqual(pathIssues(soft.errors), []);
|
|
});
|
|
|
|
test('1.9: "`tests/a.test.mjs` - covers `src/parse.mjs`" names only the test file', () => {
|
|
const r = entryPaths('`tests/a.test.mjs` - covers `src/parse.mjs`');
|
|
assert.deepEqual(r.files, ['tests/a.test.mjs']);
|
|
assert.deepEqual(r.issues, []);
|
|
const v = validatePlanContent(plan19('`tests/a.test.mjs` - covers `src/parse.mjs`'));
|
|
assert.deepEqual(pathIssues(v.errors), [], JSON.stringify(v.errors));
|
|
});
|
|
|
|
test('1.9: several leading paths separated by commas or whitespace are all files', () => {
|
|
assert.deepEqual(entryPaths('`src/a.mjs`, `./src/b.mjs` `tests/a.test.mjs` *(new)*, `src/c.mjs`').files,
|
|
['src/a.mjs', 'src/b.mjs', 'tests/a.test.mjs']);
|
|
});
|
|
|
|
test('1.9: a wrapped description line is not an entry', () => {
|
|
const text = '`src/a.mjs` - the parser, which\n reads src/b.mjs and the CLI options';
|
|
const r = entryPaths(text);
|
|
assert.deepEqual(r.files, ['src/a.mjs']);
|
|
assert.deepEqual(r.issues, []);
|
|
const v = validatePlanContent(plan19(text));
|
|
assert.deepEqual(pathIssues(v.errors), [], JSON.stringify(v.errors));
|
|
});
|
|
|
|
test('1.9: an empty first line with a nested list is not an entry; each list item is one', () => {
|
|
const text = '\n - `src/a.mjs` - the parser\n - `tests/a.test.mjs` (new)';
|
|
const r = entryPaths(text);
|
|
assert.deepEqual(r.files, ['src/a.mjs', 'tests/a.test.mjs']);
|
|
assert.deepEqual(r.issues, []);
|
|
const v = validatePlanContent(plan19(text));
|
|
assert.deepEqual(pathIssues(v.errors), [], JSON.stringify(v.errors));
|
|
// A list item that does not open with a path is refused.
|
|
const bad = entryPaths('\n - `src/a.mjs`\n - the CLI in src/cli.mjs');
|
|
assert.deepEqual(bad.files, ['src/a.mjs']);
|
|
assert.deepEqual(bad.issues.map((i) => i.code), ['PLAN_STEP_PATH_NOT_FIRST']);
|
|
assert.match(bad.issues[0].entry, /the CLI in src\/cli\.mjs/);
|
|
});
|
|
|
|
test('1.9: a leading `parse()` is refused as no path', () => {
|
|
const r = entryPaths('`parse()` - the parser entry');
|
|
assert.deepEqual(r.files, []);
|
|
assert.deepEqual(r.issues.map((i) => i.code), ['PLAN_STEP_PATH_NOT_A_PATH']);
|
|
const v = validatePlanContent(plan19('`parse()` - the parser entry'));
|
|
assert.ok(errCodes(v).includes('PLAN_STEP_PATH_NOT_A_PATH'), errCodes(v).join(','));
|
|
});
|
|
|
|
test('1.9: the shape test is the reader\'s leading-position test', () => {
|
|
assert.equal(typeof schema.isPathShape, 'function', 'plan-schema exports isPathShape');
|
|
for (const s of ['LICENSE', 'Makefile', 'src/a.mjs', 'package.json', 'docs/']) assert.equal(schema.isPathShape(s), true, s);
|
|
for (const s of ['parse()', '-x', 'https://example.com/a.md', 'format']) assert.equal(schema.isPathShape(s), false, s);
|
|
});
|
|
|
|
test('1.9: `none` and an absent `Files:` are refused', () => {
|
|
const none = entryPaths('none');
|
|
assert.deepEqual(none.files, []);
|
|
assert.deepEqual(none.issues.map((i) => i.code), ['PLAN_STEP_PATH_NOT_FIRST']);
|
|
const empty = entryPaths('');
|
|
assert.deepEqual(empty.files, []);
|
|
assert.deepEqual(empty.issues.map((i) => i.code), ['PLAN_STEP_PATH_NOT_FIRST']);
|
|
assert.ok(errCodes(validatePlanContent(plan19('none'))).includes('PLAN_STEP_PATH_NOT_FIRST'));
|
|
const absent = buildPlan({ version: '1.9' }).replace(/^- \*\*Files:\*\* `src\/mod1\.mjs`\n/m, '');
|
|
assert.ok(!/Files:\*\* `src\/mod1\.mjs`/.test(absent), 'the fixture drops Step 1\'s Files line');
|
|
const r = validatePlanContent(absent);
|
|
const e = r.errors.find((i) => i.code === 'PLAN_STEP_PATH_NOT_FIRST');
|
|
assert.ok(e, errCodes(r).join(','));
|
|
assert.match(e.message, /Step 1/);
|
|
});
|
|
|
|
test('1.9: the tests-only check reads Files by the grammar', () => {
|
|
const body = '- **Files:** `tests/a.test.mjs` - covers `src/parse.mjs`\n- **Changes:** x\n';
|
|
assert.deepEqual(schema.stepTestsOnly(body, '1.9'), { testsOnly: true, files: ['tests/a.test.mjs'] });
|
|
});
|
|
|
|
test('below 1.9: an unticked path gets PLAN_STEP_PATH_UNTICKED (a warning under --strict too), the 6.3.1 paths, and the gate passes', () => {
|
|
const files = '`tests/mod1.test.mjs`, src/mod1.mjs';
|
|
const text = buildPlan({ steps: [{ filesRaw: files }, { endToEnd: 'bash tests/e2e.sh' }] });
|
|
assert.ok(text.includes(`- **Files:** ${files}\n`), 'the builder writes the raw Files value');
|
|
const r = validatePlanContent(text);
|
|
assert.deepEqual(r.errors, [], JSON.stringify(r.errors, null, 2));
|
|
const w = r.warnings.filter((i) => i.code === 'PLAN_STEP_PATH_UNTICKED');
|
|
assert.equal(w.length, 1, warnCodes(r).join(','));
|
|
assert.match(w[0].message, /Step 1/);
|
|
assert.match(w[0].message, /src\/mod1\.mjs/);
|
|
assert.equal(r.warnings.filter((i) => PATH_CODES.includes(i.code)).length, 0);
|
|
const soft = validatePlanContent(text, { strict: false });
|
|
assert.ok(warnCodes(soft).includes('PLAN_STEP_PATH_UNTICKED'));
|
|
// Read as before: the same paths 6.3.1 reads.
|
|
assert.equal(typeof schema.untickedPaths, 'function', 'plan-schema exports untickedPaths');
|
|
assert.deepEqual(schema.untickedPaths(files), ['src/mod1.mjs']);
|
|
assert.deepEqual(schema.fieldPaths(files), ['tests/mod1.test.mjs', 'src/mod1.mjs']);
|
|
assert.deepEqual(schema.stepTestsOnly(`- **Files:** ${files}\n`).files, ['tests/mod1.test.mjs', 'src/mod1.mjs']);
|
|
// The gate passes it: a warning never stops a plan.
|
|
const rec = passedCritic(text);
|
|
assert.equal(rec.code, 0, rec.message);
|
|
const g = gatePlan(rec.text);
|
|
assert.equal(g.code, 0, JSON.stringify(g.reasons));
|
|
});
|
|
|
|
test('below 1.9: a plan whose Files are all backticked gets no new warning', () => {
|
|
const r = validatePlanContent(buildPlan());
|
|
assert.equal(r.warnings.find((w) => w.code === 'PLAN_STEP_PATH_UNTICKED'), undefined, JSON.stringify(r.warnings));
|
|
// The same unticked step in a 1.9 plan is refused rather than warned about.
|
|
const v = validatePlanContent(plan19('src/mod1.mjs'));
|
|
assert.ok(errCodes(v).includes('PLAN_STEP_PATH_NOT_FIRST'), errCodes(v).join(','));
|
|
assert.equal(v.warnings.find((w) => w.code === 'PLAN_STEP_PATH_UNTICKED'), undefined);
|
|
});
|