fix(execute): every test file on the Test first line is read, locked and copied

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-12 14:42:12 +02:00
commit 8bf97cc834
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 129 additions and 13 deletions

View file

@ -486,15 +486,44 @@ export function stepTestsOnly(body) {
return stepTestsOnlyFromFields(parseStepFields(body));
}
// A line of the `Test first:` field that names its test files (6.3.1's selector).
const TEST_FIRST_FILE_LINE = /\bFile:\**[ \t]*`/i;
/**
* Whether a path counts as a test for this step: its own `Test first:` file, or
* a path that is clearly a test.
* The step's test files: every file on the `Test first:` `File:` line. A line
* counts as that line exactly when TEST_FIRST_FILE_LINE matches it. Below
* plan_version 1.9 (or without a version) every backticked span on the line,
* from the match on, that has the shape of a path is a test file (a call
* `parse()`, an option, a URL is not), normalised and deduplicated. A
* `Verifies:` line without a `File:` is never read. Every version reads this
* way until the 1.9 branch exists.
* @param {Map<string, {text: string}>} fields - parseStepFields(body)
* @param {string|null} [version] - the plan's plan_version
* @returns {string[]}
*/
export function testFirstFiles(fields, version) {
if (!fields || !fields.has('Test first')) return [];
const out = [];
for (const line of String(fields.get('Test first').text ?? '').split('\n')) {
const at = TEST_FIRST_FILE_LINE.exec(line);
if (!at) continue;
for (const m of line.slice(at.index).matchAll(/`([^`\n]+)`/g)) {
const s = m[1].trim();
if (!(tickedPath(s) || BUILD_FILE.test(s))) continue;
const p = norm(s);
if (!out.includes(p)) out.push(p);
}
}
return out;
}
/**
* Whether a path counts as a test for this step: its own `Test first:` files
* (testFirstFiles), or a path that is clearly a test.
* @returns {(path: string) => boolean}
*/
function stepTestFile(f) {
const testFirst = f.has('Test first')
? [...f.get('Test first').text.matchAll(/\bFile:\**[ \t]*`([^`\n]+)`/gi)].map((m) => norm(m[1].trim()))
: [];
function stepTestFile(f, version) {
const testFirst = testFirstFiles(f, version);
return (p) => testFirst.includes(p) ||
(isTestPath(p) && (UNDER_TESTS_RE.test(p) || !(DOC_RE.test(p) || DATA_RE.test(p))));
}