fix(plan): below 1.9 a test file after a note, and, plus or + is read

Operator decisions 28 (13.10) and 31 (14.10); review finding bd2520b0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-14 10:44:47 +02:00
commit 8e6229b01d
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 122 additions and 6 deletions

View file

@ -13,6 +13,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as m from '../../lib/parsers/plan-schema.mjs';
import { validatePlanContent } from '../../lib/validators/plan-validator.mjs';
import { isTestPath } from '../../lib/util/test-path.mjs';
import { buildPlan } from '../helpers/plan-v18-builder.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
@ -187,6 +188,67 @@ test('decision 20: a 1.8 line with two File: markers, each followed by a test, g
assert.deepEqual(testFirstFiles(body, null), ['tests/a.test.mjs', 'tests/b.test.mjs']);
});
// ---- operator decisions 28 (13.10) and 31 (14.10), review bd2520b0 ---------
// Below 1.9 the run after `File:` also goes on past a note in brackets
// (`(new)`, `*(new)*`, `(existing ...)`) and the whole words `and`, `plus` and
// `+`, to a further path only when it is clearly a test; a path that is not
// ends the run, and so does any other text. At 1.9 nothing changes.
const OLDER = ['1.8', '1.7', null];
const fileLine = (rest) => step('`tests/a.test.mjs`, `tests/b.test.mjs`', [` - File: ${rest}`, ' - Verifies: x']);
test('decisions 28 and 31: below 1.9 a test after a note, and, plus or + is read', () => {
const lines = [
'`tests/a.test.mjs` (new), `tests/b.test.mjs` (new),',
'`tests/a.test.mjs` and `tests/b.test.mjs`',
'`tests/a.test.mjs` (existing) + `tests/b.test.mjs`',
'`tests/a.test.mjs` (existing, the parser) plus `tests/b.test.mjs`',
'`tests/a.test.mjs` *(new)*, `tests/b.test.mjs`',
];
for (const rest of lines) {
for (const v of OLDER) {
assert.deepEqual(testFirstFiles(fileLine(rest), v), ['tests/a.test.mjs', 'tests/b.test.mjs'], `version ${v}: ${rest}`);
}
}
});
test('decisions 28 and 31: below 1.9 a near miss gives only the first test', () => {
// Both read as tests by lib/util/test-path.mjs alone (a specs/ folder); the
// run refuses them all the same: a document or a data file outside tests/.
assert.equal(isTestPath('docs/specs/api.md'), true);
assert.equal(isTestPath('api/specs/openapi.yaml'), true);
const lines = [
'`tests/a.test.mjs` (new) - covers `src/a.mjs`',
'`tests/a.test.mjs` (new), `src/a.mjs`',
'`tests/a.test.mjs` (new) or `tests/b.test.mjs`',
'`tests/a.test.mjs` (new), covers `tests/b.test.mjs`',
'`tests/a.test.mjs` (new) and `src/a.mjs`',
'`tests/a.test.mjs` (new) and `https://example.invalid/tests/b.test.mjs`',
'`tests/a.test.mjs` (new) and `docs/specs/api.md`',
'`tests/a.test.mjs` (new), `api/specs/openapi.yaml`',
'`tests/a.test.mjs` android `tests/b.test.mjs`',
'`tests/a.test.mjs` andand `tests/b.test.mjs`',
'`tests/a.test.mjs` plusand `tests/b.test.mjs`',
];
for (const rest of lines) {
for (const v of OLDER) {
assert.deepEqual(testFirstFiles(fileLine(rest), v), ['tests/a.test.mjs'], `version ${v}: ${rest}`);
}
}
});
test('decisions 28 and 31: below 1.9 the run after a note stops at the first source path', () => {
const rest = '`tests/a.test.mjs` (new), `tests/b.test.mjs`, `src/a.mjs`, `tests/c.test.mjs`';
for (const v of OLDER) {
assert.deepEqual(testFirstFiles(fileLine(rest), v), ['tests/a.test.mjs', 'tests/b.test.mjs'], `version ${v}`);
}
});
test('decisions 28 and 31: at 1.9 the two-test line with notes still gives only the first test', () => {
const body = fileLine('`tests/a.test.mjs` (new), `tests/b.test.mjs` (new),');
assert.deepEqual(testFirstFiles(body, '1.9'), ['tests/a.test.mjs']);
});
// ---- one definition in the prose (plan step 11, review cf241526) -----------
// The executor and the step helper define the step's test files once, as
// decision 20 reads them; every other place points at that definition. Red