Review findings b4fa1927 and 4b6460e5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
331 lines
19 KiB
JavaScript
331 lines
19 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';
|
|
import { plannedPaths } from '../../lib/verification/plan-scope-sweep.mjs';
|
|
import { childEnv } from '../helpers/child-env.mjs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
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 is the current target: PLAN_VERSION_CURRENT === \'1.9\'', () => {
|
|
assert.equal(schema.PLAN_VERSION_CURRENT, '1.9');
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
// ---- Step 6: K6 and the red proof read a 1.9 plan by the grammar ----
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const RED_PROOF = join(ROOT, 'lib', 'verification', 'red-proof.mjs');
|
|
const COVERS = '`tests/a.test.mjs` - covers `src/parse.mjs`';
|
|
// Step 1 names only its test, and the description mentions the source it covers.
|
|
// The manifest names the test alone, so it adds nothing K6 would not plan anyway.
|
|
const coversPlan = (version) => buildPlan({ version, steps: [
|
|
{ filesRaw: COVERS, test: 'tests/a.test.mjs', manifestPaths: ['tests/a.test.mjs'] },
|
|
{ endToEnd: 'bash tests/e2e.sh' },
|
|
] });
|
|
|
|
test('1.9: K6 plans the files a 1.9 Files: entry names, never a path its description mentions', () => {
|
|
const text = coversPlan('1.9');
|
|
assert.ok(text.includes(`- **Files:** ${COVERS}\n`), 'the builder writes the raw Files value');
|
|
assert.deepEqual(plannedPaths(text), ['src/mod2.mjs', 'tests/a.test.mjs', 'tests/mod2.test.mjs']);
|
|
});
|
|
|
|
test('1.9: K6 plans the files a 1.9 Test first File: line names, never its description', () => {
|
|
const text = buildPlan({ version: '1.9', steps: [
|
|
{ filesRaw: '`tests/a.test.mjs`', testFirstRaw: ` - File: ${COVERS}`, test: 'tests/a.test.mjs', manifestPaths: ['tests/a.test.mjs'] },
|
|
{ endToEnd: 'bash tests/e2e.sh' },
|
|
] });
|
|
assert.ok(text.includes(` - File: ${COVERS}\n`), 'the builder writes the raw File line');
|
|
assert.deepEqual(plannedPaths(text), ['src/mod2.mjs', 'tests/a.test.mjs', 'tests/mod2.test.mjs']);
|
|
});
|
|
|
|
test('below 1.9: K6 plans what it planned before - the same input plans both paths', () => {
|
|
assert.deepEqual(plannedPaths(coversPlan('1.8')), ['src/mod2.mjs', 'src/parse.mjs', 'tests/a.test.mjs', 'tests/mod2.test.mjs']);
|
|
const tf = buildPlan({ version: '1.8', steps: [
|
|
{ filesRaw: '`tests/a.test.mjs`', testFirstRaw: ` - File: ${COVERS}`, test: 'tests/a.test.mjs', manifestPaths: ['tests/a.test.mjs'] },
|
|
{ endToEnd: 'bash tests/e2e.sh' },
|
|
] });
|
|
assert.deepEqual(plannedPaths(tf), ['src/mod2.mjs', 'src/parse.mjs', 'tests/a.test.mjs', 'tests/mod2.test.mjs']);
|
|
});
|
|
|
|
test('1.9: the red proof\'s tests_only reads Files by the grammar (CLI, temp git repo); 1.8 reads it as before', () => {
|
|
const repo = mkdtempSync(join(tmpdir(), 'plan19-redproof-'));
|
|
const git = (...args) => spawnSync('git', args, { cwd: repo, encoding: 'utf8', env: childEnv() });
|
|
try {
|
|
for (const args of [['init', '-q'], ['config', 'user.email', 't@example.invalid'], ['config', 'user.name', 't'], ['config', 'commit.gpgsign', 'false']]) {
|
|
assert.equal(git(...args).status, 0);
|
|
}
|
|
writeFileSync(join(repo, 'README.md'), 'x\n');
|
|
assert.equal(git('add', '-A').status, 0);
|
|
assert.equal(git('commit', '-q', '-m', 'base').status, 0);
|
|
const base = git('rev-parse', 'HEAD').stdout.trim();
|
|
// The helper wrote the step's test alone; it passes at the start point.
|
|
mkdirSync(join(repo, 'tests'));
|
|
writeFileSync(join(repo, 'tests', 'a.test.mjs'), "import { test } from 'node:test';\ntest('a', () => {});\n");
|
|
mkdirSync(join(repo, 'plans'));
|
|
for (const [version, want] of [['1.9', true], ['1.8', false]]) {
|
|
const plan = join(repo, 'plans', `plan-${version}.md`);
|
|
writeFileSync(plan, coversPlan(version));
|
|
const r = spawnSync('node', [RED_PROOF, '--red-only', '--base', base, '--cmd', 'node --test tests/a.test.mjs',
|
|
'--tests', 'tests/a.test.mjs', '--plan', plan, '--step', '1', '--json'], { cwd: repo, encoding: 'utf8', env: childEnv() });
|
|
assert.equal(r.status, 1, `${version}: ${r.stdout}${r.stderr}`);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.equal(out.reason, 'not-red-at-base', JSON.stringify(out));
|
|
assert.equal(out.tests_only, want, `plan_version ${version}: ${JSON.stringify(out.tests_only_note ?? null)}`);
|
|
}
|
|
} finally { rmSync(repo, { recursive: true, force: true }); }
|
|
});
|
|
|
|
// The validator reads the step's test the way the executor does (S139): from
|
|
// 1.9 `Test first:` names its test only through a line that opens with
|
|
// `File:` and its backticked path (testFirstFiles), so a `File:` mid-line is
|
|
// no test. Below 1.9 the step check is read as before (decision 16).
|
|
const planTestFirst19 = (version, line) =>
|
|
buildPlan({ version, steps: [{ testFirstRaw: line }, { endToEnd: 'bash tests/e2e.sh' }] });
|
|
|
|
test('1.9: a Test first whose File: stands mid-line names no test - PLAN_STEP_NO_TEST_FIRST under --strict, a warning under --soft', () => {
|
|
for (const line of [
|
|
' - see File: `tests/mod1.test.mjs` for the test',
|
|
' - Pattern: File: `tests/mod1.test.mjs`',
|
|
]) {
|
|
const text = planTestFirst19('1.9', line);
|
|
assert.ok(text.includes(`${line}\n`), 'the builder writes the raw File line');
|
|
assert.deepEqual(schema.testFirstFiles(schema.parseStepFields(text.split('### Step 1:')[1].split('### Step 2:')[0]), '1.9'), [],
|
|
'the executor reads no test file here');
|
|
const r = validatePlanContent(text, { strict: true });
|
|
assert.equal(r.valid, false, `${line}: ${errCodes(r).join(',')}`);
|
|
const no = r.errors.filter((e) => e.code === 'PLAN_STEP_NO_TEST_FIRST');
|
|
assert.equal(no.length, 1, `${line}: ${errCodes(r).join(',')}`);
|
|
assert.match(no[0].message, /Step 1/);
|
|
const soft = validatePlanContent(text, { strict: false });
|
|
assert.ok(warnCodes(soft).includes('PLAN_STEP_NO_TEST_FIRST'), `${line}: ${warnCodes(soft).join(',')}`);
|
|
}
|
|
});
|
|
|
|
test('1.9: a File: line that opens the line names its test - no PLAN_STEP_NO_TEST_FIRST', () => {
|
|
for (const line of [
|
|
' - File: `tests/mod1.test.mjs` (new)',
|
|
' - **File:** `tests/mod1.test.mjs` *(new)*',
|
|
' - File: `tests/mod1.test.mjs`, `tests/mod1b.test.mjs` (new)',
|
|
]) {
|
|
const r = validatePlanContent(planTestFirst19('1.9', line), { strict: true });
|
|
assert.equal(r.errors.find((e) => e.code === 'PLAN_STEP_NO_TEST_FIRST'), undefined, `${line}: ${errCodes(r).join(',')}`);
|
|
}
|
|
});
|
|
|
|
test('below 1.9: the step check reads a mid-line File: as before', () => {
|
|
const r = validatePlanContent(planTestFirst19('1.8', ' - see File: `tests/mod1.test.mjs` for the test'), { strict: true });
|
|
assert.equal(r.errors.find((e) => e.code === 'PLAN_STEP_NO_TEST_FIRST'), undefined, errCodes(r).join(','));
|
|
});
|
|
|
|
// ---- Step 3 (trial 4): a wrapped 1.9 entry keeps its paths, and a numbered
|
|
// `File:` line names its test (reviews b4fa1927, 4b6460e5) ----
|
|
|
|
const WRAPPED = '`tests/a.test.mjs`,\n `src/a.mjs`';
|
|
const step1Fields = (text) => schema.parseStepFields(text.split('### Step 1:')[1].split('### Step 2:')[0]);
|
|
|
|
test('1.9: a Files list wrapped after a comma names both files and no issue', () => {
|
|
const r = entryPaths(WRAPPED);
|
|
assert.deepEqual(r.files, ['tests/a.test.mjs', 'src/a.mjs']);
|
|
assert.deepEqual(r.issues, []);
|
|
const v = validatePlanContent(plan19(WRAPPED));
|
|
assert.deepEqual(pathIssues(v.errors), [], JSON.stringify(v.errors));
|
|
// An issue keeps the entry's first line number.
|
|
const bad = entryPaths('- `src/a.mjs`\n- the CLI,\n `src/b.mjs`');
|
|
assert.deepEqual(bad.files, ['src/a.mjs']);
|
|
assert.deepEqual(bad.issues.map((i) => [i.code, i.line]), [['PLAN_STEP_PATH_NOT_FIRST', 2]]);
|
|
});
|
|
|
|
test('1.9: a step whose wrapped Files list names its source is not tests-only', () => {
|
|
const body = `- **Files:** ${WRAPPED}\n- **Changes:** x\n- **Test first:**\n - File: \`tests/a.test.mjs\` (new)\n - Verifies: x\n`;
|
|
assert.deepEqual(schema.stepTestsOnly(body, '1.9'), { testsOnly: false, files: ['tests/a.test.mjs', 'src/a.mjs'] });
|
|
});
|
|
|
|
test('1.9: K6 plans the second file of a wrapped Files list', () => {
|
|
const text = buildPlan({ version: '1.9', steps: [
|
|
{ filesRaw: WRAPPED, test: 'tests/a.test.mjs', manifestPaths: ['tests/a.test.mjs'] },
|
|
{ endToEnd: 'bash tests/e2e.sh' },
|
|
] });
|
|
assert.ok(text.includes(`- **Files:** ${WRAPPED}\n`), 'the builder writes the raw Files value');
|
|
assert.ok(plannedPaths(text).includes('src/a.mjs'), JSON.stringify(plannedPaths(text)));
|
|
});
|
|
|
|
test('1.9: a numbered File line (`1.` and `1)`) names its test - no PLAN_STEP_NO_TEST_FIRST', () => {
|
|
for (const line of [' 1. File: `tests/mod1.test.mjs` (new)', ' 1) File: `tests/mod1.test.mjs` (new)']) {
|
|
const text = planTestFirst19('1.9', line);
|
|
assert.ok(text.includes(`${line}\n`), 'the builder writes the raw File line');
|
|
assert.deepEqual(schema.testFirstFiles(step1Fields(text), '1.9'), ['tests/mod1.test.mjs'], line);
|
|
const r = validatePlanContent(text, { strict: true });
|
|
assert.equal(r.errors.find((e) => e.code === 'PLAN_STEP_NO_TEST_FIRST'), undefined, `${line}: ${errCodes(r).join(',')}`);
|
|
}
|
|
});
|
|
|
|
test('1.9: a wrapped File line keeps its second test; a wrapped description stays description', () => {
|
|
const wrapped = planTestFirst19('1.9', ' - File: `tests/mod1.test.mjs`,\n `tests/mod1b.test.mjs` (new)');
|
|
assert.deepEqual(schema.testFirstFiles(step1Fields(wrapped), '1.9'), ['tests/mod1.test.mjs', 'tests/mod1b.test.mjs']);
|
|
const described = planTestFirst19('1.9', ' - File: `tests/mod1.test.mjs` - covers\n `src/parse.mjs` and the CLI');
|
|
assert.deepEqual(schema.testFirstFiles(step1Fields(described), '1.9'), ['tests/mod1.test.mjs']);
|
|
});
|
|
|
|
test('1.9: a Verifies item that wraps onto a backticked source path gives only the File line\'s test', () => {
|
|
const text = planTestFirst19('1.9',
|
|
' - File: `tests/mod1.test.mjs` (new)\n - Verifies: the parser in\n `src/mod1.mjs` reads the CLI options');
|
|
assert.deepEqual(schema.testFirstFiles(step1Fields(text), '1.9'), ['tests/mod1.test.mjs']);
|
|
});
|
|
|
|
test('1.9: two unmarked File lines in a row give both tests', () => {
|
|
const text = planTestFirst19('1.9', ' File: `tests/mod1.test.mjs`\n File: `tests/mod1b.test.mjs`');
|
|
assert.deepEqual(schema.testFirstFiles(step1Fields(text), '1.9'), ['tests/mod1.test.mjs', 'tests/mod1b.test.mjs']);
|
|
});
|