fix: round 6 of the release review of 6.3.0
Round 6 (v6.2.0..c3641ff, WARN: 11 MAJOR, 3 MINOR; 0 BLOCKER). The round-5
fixes hold (code-correctness-reviewer); three of the new MAJOR were side
effects of running checks with no shell. Each fixed behind a test that
failed first on c3641ff.
- git checks stay in the repo too: no path that climbs out with .., starts
at / or reads <rev>:../x, and no --no-index (both reviewers).
- A trailing `# comment` on a shell-fence line is dropped: with no shell,
its words made an honest check NOT RUN, among them the first check of the
shipped example plan.
- A glob reaches `node --test` only on Node 21 and later, which expands it
itself; on an older Node it is refused with that reason (engines >=18).
- With no shell, exit 126/127 is the test program's own: red proof reads it
as a red (the control run still tells an environment fault apart), the
done line and the test lock as a failure.
- A runner that could not start (not installed) is NOT RUN in the criteria
runner and not-run (exit 2) in regression-check, as red-proof and
done-line already said; never a red suite.
- Tests-only: a backticked `Makefile` alone in its entry is a path, and red
proof confirms the plan's tests-only reading against the step's own diff
(the altitude finding: the diff is a fact, the Files: prose is not).
- release-check stops a suite that never ends (15 minutes) and reads it as
not measured (exit 2).
- The two lines this release added to pre-bash-executor.mjs are ASCII.
- MINOR: git and the other runners share one plain-word pattern.
Not in this round: MINOR f00a2eb8 (shared norm/ticked-path and isMain
helpers) and MINOR on plan-review-dedup's repeated tokenizing.
Suite 2194 (2192/0/2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c3641ffa42
commit
45aa9e107a
17 changed files with 262 additions and 62 deletions
|
|
@ -293,7 +293,8 @@ export function onFailureAction(text) {
|
|||
*
|
||||
* - A field with a backticked path names its backticked spans that pass the
|
||||
* path test (a `.`, `/` or `\`; no URL, option or call like `parse()`),
|
||||
* wherever they sit (`a.mjs`/`b.mjs`, inside a note). Its descriptions are
|
||||
* wherever they sit (`a.mjs`/`b.mjs`, inside a note), and a backticked name
|
||||
* with no extension that stands alone in its entry (`Makefile`). Its descriptions are
|
||||
* not read: "covers pass/fail", "JSON, YAML" or a directory named in passing
|
||||
* is no path. An entry of its own with no backticks (", .gitignore") counts
|
||||
* by its first word, and only when that is clearly a file or a directory.
|
||||
|
|
@ -331,8 +332,11 @@ export function fieldPaths(text) {
|
|||
for (const raw of masked.split(/[,\n]/)) {
|
||||
const entry = raw.trim().replace(/^[-*]\s+/, '');
|
||||
if (entry === '') continue;
|
||||
const ticked = [...entry.matchAll(/\u0000(\d+)\u0000/g)].map((m) => Number(m[1])).filter((i) => isTicked[i]);
|
||||
const inEntry = [...entry.matchAll(/\u0000(\d+)\u0000/g)].map((m) => Number(m[1]));
|
||||
const ticked = inEntry.filter((i) => isTicked[i]);
|
||||
if (ticked.length) { out.push(...ticked.map((i) => spans[i])); continue; }
|
||||
// A backticked `Makefile` alone in its entry is a file (round 6).
|
||||
if (inEntry.length === 1 && /^\u0000\d+\u0000$/.test(entry) && BARE_NAME.test(spans[inEntry[0]])) { out.push(spans[inEntry[0]]); continue; }
|
||||
const words = entry.split(/\s+/).filter((w) => !w.includes('\u0000')).map(plainWord).filter(Boolean);
|
||||
if (tickedField) {
|
||||
const first = plainWord(entry.split(/\s+/)[0]);
|
||||
|
|
@ -371,15 +375,23 @@ export function stepTestsOnly(body) {
|
|||
return stepTestsOnlyFromFields(parseStepFields(body));
|
||||
}
|
||||
|
||||
/** `stepTestsOnly` for a caller that already holds the step's parsed fields. */
|
||||
export function stepTestsOnlyFromFields(f) {
|
||||
const files = f.has('Files') ? fieldPaths(f.get('Files').text) : [];
|
||||
/**
|
||||
* Whether a path counts as a test for this step: its own `Test first:` file, or
|
||||
* a path that is clearly a test. red-proof.mjs uses it on the step's diff.
|
||||
* @returns {(path: string) => boolean}
|
||||
*/
|
||||
export 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()))
|
||||
: [];
|
||||
const isTest = (p) => testFirst.includes(p) ||
|
||||
return (p) => testFirst.includes(p) ||
|
||||
(isTestPath(p) && (UNDER_TESTS_RE.test(p) || !(DOC_RE.test(p) || DATA_RE.test(p))));
|
||||
return { testsOnly: files.length > 0 && files.every(isTest), files };
|
||||
}
|
||||
|
||||
/** `stepTestsOnly` for a caller that already holds the step's parsed fields. */
|
||||
export function stepTestsOnlyFromFields(f) {
|
||||
const files = f.has('Files') ? fieldPaths(f.get('Files').text) : [];
|
||||
return { testsOnly: files.length > 0 && files.every(stepTestFile(f)), files };
|
||||
}
|
||||
|
||||
// ---- sections (fence-aware) ------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -180,7 +180,8 @@ function main(argv) {
|
|||
if (!screen.allowed) fail(2, `command blocked: ${screen.rule}`);
|
||||
// No shell: the words the screen checked are the words that run.
|
||||
const r = runCommand(testCmd, { cwd: cur.repo.root, timeoutMs: DEFAULT_TIMEOUT_MS });
|
||||
if (r.error || r.status === null || r.status === 126 || r.status === 127) {
|
||||
// With no shell, 126/127 is the test program's own exit: a red, not "could not run".
|
||||
if (r.error || r.status === null) {
|
||||
fail(2, `the test command did not run to an exit code (${r.error?.message ?? r.signal ?? `exit ${r.status}`})`);
|
||||
}
|
||||
if (r.status !== 0) fail(1, `the test command exited ${r.status}; the lock stays unmarked`);
|
||||
|
|
|
|||
|
|
@ -161,11 +161,14 @@ export function firstCommand(text) {
|
|||
// A command line inside a shell-tagged fence. A console block may prefix the
|
||||
// line with a `$` or `>` prompt; blank and comment-only lines declare nothing.
|
||||
// A `#` root prompt is NOT stripped: it is indistinguishable from a comment,
|
||||
// and running a comment is the worse of the two mistakes.
|
||||
// and running a comment is the worse of the two mistakes. A trailing comment
|
||||
// (`npm test # all green`) is not part of the command: a shell dropped it,
|
||||
// and with no shell (runCommand) its words would reach the screen and make an
|
||||
// honest check NOT RUN (release review of 6.3.0, round 6).
|
||||
function fencedCommand(text) {
|
||||
const line = text.trim();
|
||||
if (line === '' || line.startsWith('#')) return null;
|
||||
return line.replace(/^[$>]\s+/, '');
|
||||
return line.replace(/^[$>]\s+/, '').replace(/\s+#.*$/, '');
|
||||
}
|
||||
|
||||
function parseSection(markdown, heading, prefix) {
|
||||
|
|
@ -292,9 +295,6 @@ const SHELL_OPERATORS = /[;&|<>]|\$\(|`/;
|
|||
|
||||
// Read-only git subcommands: they report, they do not write a repository.
|
||||
const READONLY_GIT = new Set(['status', 'log', 'diff', 'show', 'ls-files']);
|
||||
// A word that means the same to git with or without a shell (refs like HEAD~1
|
||||
// and HEAD^, formats like %h, paths).
|
||||
const GIT_LITERAL_WORD = /^[A-Za-z0-9_@%^~:.,=+/-]+$/;
|
||||
// `--output`, `--output=<file>`, or an abbreviation of it git may accept.
|
||||
function writesOutput(word) {
|
||||
const name = word.split('=')[0];
|
||||
|
|
@ -304,14 +304,18 @@ function writesOutput(word) {
|
|||
const deny = (reason) => ({ allowed: false, reason });
|
||||
const permit = () => ({ allowed: true, reason: '' });
|
||||
|
||||
// A word that means the same with or without a shell.
|
||||
// A word that means the same with or without a shell (also git's refs like
|
||||
// HEAD~1 and HEAD^, formats like %h, paths).
|
||||
const PLAIN_WORD = /^[A-Za-z0-9_@%^~:.,=+/-]+$/;
|
||||
// The same, with a glob in it: passed on only to `node --test`, which expands
|
||||
// its own patterns (sh would have expanded `.?` to `..`).
|
||||
// its own patterns from Node 21 on (sh would have expanded `.?` to `..`).
|
||||
const GLOB_WORD = /^[A-Za-z0-9_./][A-Za-z0-9_@%^~:.,=+/*?-]*$/;
|
||||
const takesGlobs = (t) => t[0] === 'node' && t[1] === '--test';
|
||||
const NODE_GLOB_MAJOR = 21;
|
||||
const runningNodeMajor = () => Number(process.versions.node.split('.')[0]);
|
||||
// A path outside the repo: one that climbs with `..` or starts at `/`.
|
||||
const leavesRepo = (w) => w.startsWith('/') || w.split('/').includes('..');
|
||||
// The same for a git word, which may also name a path as `<rev>:<path>`.
|
||||
const gitLeavesRepo = (w) => leavesRepo(w) || (w.includes(':') && leavesRepo(w.slice(w.indexOf(':') + 1)));
|
||||
|
||||
// After each runner's fixed words: the options it may take (`name` or
|
||||
// `name=value`; `value` lists the ones whose value may be the next word, and
|
||||
|
|
@ -415,9 +419,13 @@ const RUNNERS = {
|
|||
// diff/log/show write to a file (release review of 6.3.0, 2026-10-10: a
|
||||
// plan's check could overwrite any file, first plainly, then through
|
||||
// quoting while a shell still ran it).
|
||||
const odd = t.find((w) => !GIT_LITERAL_WORD.test(w));
|
||||
const odd = t.find((w) => !PLAIN_WORD.test(w));
|
||||
if (odd !== undefined) return deny(`git with a word that means something else in a shell (${odd}): only plain words run, as written`);
|
||||
if (t.some(writesOutput)) return deny('git with --output writes a file, so it is not a read-only check');
|
||||
// A check runs in the repo: no path that climbs out or starts at /, and no
|
||||
// --no-index, which makes git diff compare any two files (round 6).
|
||||
const out = t.slice(2).find((w) => w === '--no-index' || (!w.startsWith('-') && gitLeavesRepo(w)));
|
||||
if (out !== undefined) return deny(`git with a path outside the repo (${out}): a check runs in the repo`);
|
||||
return permit();
|
||||
},
|
||||
};
|
||||
|
|
@ -463,12 +471,18 @@ function screenShape(command, opts, writeTime) {
|
|||
const form = RUNNERS[tokens[0]](tokens, cwd, writeTime);
|
||||
if (!form.allowed || tokens[0] === 'git') return form;
|
||||
const words = tokens.slice(1);
|
||||
const odd = words.find((w) => !PLAIN_WORD.test(w) && !(takesGlobs(tokens) && GLOB_WORD.test(w)));
|
||||
const nodeTest = tokens[0] === 'node' && tokens[1] === '--test';
|
||||
const nodeMajor = opts.nodeMajor ?? runningNodeMajor();
|
||||
const odd = words.find((w) => !PLAIN_WORD.test(w) && !(nodeTest && GLOB_WORD.test(w)));
|
||||
if (odd !== undefined) {
|
||||
return deny(GLOB_WORD.test(odd)
|
||||
? `a glob (${odd}): no shell runs this, and \`${tokens[0]}\` does not expand one itself; name the files, or use \`node --test\``
|
||||
: `a word that means something else in a shell (${odd}): only plain words run, as written`);
|
||||
}
|
||||
const glob = nodeTest && nodeMajor < NODE_GLOB_MAJOR ? words.find((w) => !PLAIN_WORD.test(w)) : undefined;
|
||||
if (glob !== undefined) {
|
||||
return deny(`a glob (${glob}): node ${nodeMajor} does not expand one itself (Node ${NODE_GLOB_MAJOR} and later do), and no shell runs this; name the files`);
|
||||
}
|
||||
const at = words.find((w) => w.startsWith('@'));
|
||||
if (at !== undefined) return deny(`a word that starts with @ (${at}): pytest reads @file as a file of more arguments`);
|
||||
const out = words.find((w) => !w.startsWith('-') && leavesRepo(w));
|
||||
|
|
@ -496,11 +510,13 @@ export function runCommand(command, { cwd, timeoutMs = DEFAULT_TIMEOUT_MS } = {}
|
|||
|
||||
// --- running ----------------------------------------------------------------
|
||||
|
||||
// A program that could not start (not installed: ENOENT) or was killed by the
|
||||
// timeout has no exit code; its error is the output, so the row says why.
|
||||
// A program that could not start (not installed: ENOENT) has no exit code and
|
||||
// measured nothing: `startError` says so, and runCriteria reports it NOT RUN.
|
||||
// One killed by the timeout did start, and a hang is a red (`failed`).
|
||||
function defaultExec(command, { cwd, timeoutMs }) {
|
||||
const r = runCommand(command, { cwd, timeoutMs });
|
||||
const why = r.error ? `${r.error.message}\n` : r.signal ? `killed (${r.signal})\n` : '';
|
||||
if (r.status === null && r.signal === null && r.error) return { status: null, stdout: '', stderr: '', startError: r.error.message };
|
||||
const why = r.signal ? `killed (${r.signal})\n` : '';
|
||||
return { status: r.status, stdout: r.stdout ?? '', stderr: `${r.stderr ?? ''}${why}` };
|
||||
}
|
||||
|
||||
|
|
@ -513,9 +529,10 @@ function cap(text, max) {
|
|||
* Run each criterion that has a command. Returns one result per criterion:
|
||||
* passed the command exited 0
|
||||
* failed the command exited non-zero (or was killed by the timeout)
|
||||
* blocked the executor denylist denied it — it never reached a shell
|
||||
* blocked the executor denylist denied it — it never ran
|
||||
* unrunnable there is nothing to run: no command (placeholder or prose-only
|
||||
* criterion), or a command outside the allowlist
|
||||
* criterion), a command outside the allowlist, or a runner that
|
||||
* could not start (`startError`: not installed)
|
||||
*/
|
||||
export function runCriteria(criteria, opts = {}) {
|
||||
const exec = opts.exec ?? defaultExec;
|
||||
|
|
@ -545,6 +562,9 @@ export function runCriteria(criteria, opts = {}) {
|
|||
return { ...base, status: 'blocked', exitCode: null, output: `blocked: ${verdict.rule}` };
|
||||
}
|
||||
const r = exec(c.command, { cwd, timeoutMs });
|
||||
if (r.startError) {
|
||||
return { ...base, status: 'unrunnable', exitCode: null, output: `not runnable: the program could not start (${r.startError})`, startError: r.startError };
|
||||
}
|
||||
const output = cap(`${r.stdout ?? ''}${r.stderr ?? ''}`, maxOutput);
|
||||
return {
|
||||
...base,
|
||||
|
|
@ -766,8 +786,8 @@ export function formatCriteriaEvidence(rep) {
|
|||
out.push(
|
||||
'**NOT RUN is not a pass.** You cannot run these commands yourself — never infer, assume or ' +
|
||||
'reconstruct a result for a criterion that has none. A criterion whose result is FAILED or ' +
|
||||
'BLOCKED is `Broken`; a criterion marked NOT RUN declared no command, or a command outside ' +
|
||||
'the allowlist of test runners, so it never reached a shell — that is the ABSENCE of a ' +
|
||||
'BLOCKED is `Broken`; a criterion marked NOT RUN declared no command, a command outside ' +
|
||||
'the allowlist of test runners, or a runner that could not start, so it never ran — that is the ABSENCE of a ' +
|
||||
'measurement and never on its own a finding; a criterion with no result is judged on ' +
|
||||
'delivered code alone, and you say so in the Evidence column.'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
// appended to the step's report file. Exit 0 = written, 1 = the command failed
|
||||
// (nothing written), 2 = could not run (nothing written; never green): no
|
||||
// --step, a command outside the test-runner allowlist, a command that cannot
|
||||
// start, a working tree that is not a git repo.
|
||||
// start (not installed) or was killed, a working tree that is not a git repo.
|
||||
// With no shell, an exit 126/127 is the test program's own and a failure.
|
||||
//
|
||||
// `--check --step <N> --report <file> [--commit <sha>]`: exit 0 when the report
|
||||
// carries a well-formed line for step N (at exactly that commit, with
|
||||
|
|
@ -57,7 +58,7 @@ export function writeDoneLine({ cwd, step, cmd, report, timeoutMs = DEFAULT_TIME
|
|||
if (head.status !== 0) return { status: 'not-run', reason: `no git HEAD in ${dir}` };
|
||||
// No shell: the words the screen checked are the words that run.
|
||||
const r = runCommand(cmd, { cwd: dir, timeoutMs });
|
||||
if (r.error || r.status === null || r.status === 126 || r.status === 127) {
|
||||
if (r.error || r.status === null) {
|
||||
return { status: 'not-run', reason: `the command did not run to an exit code (${r.error?.message ?? r.signal ?? `exit ${r.status}`})`, exit: r.status };
|
||||
}
|
||||
if (r.status !== 0) return { status: 'failed', reason: `the test command exited ${r.status}`, exit: r.status };
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@
|
|||
// run, and only the full run is authoritative.
|
||||
//
|
||||
// `--plan <plan.md> --step <N>` adds `tests_only` to a `not-red-at-base`: true
|
||||
// when every file in the step's `Files:` is a test, so the step can never be
|
||||
// red and goes up to the plan (see `testsOnly` below).
|
||||
// when every file in the step's `Files:` is a test and so is every file the
|
||||
// step changed since its start point, so the step can never be red and goes
|
||||
// up to the plan (see `testsOnly` below).
|
||||
//
|
||||
// Exit 0 = proven, 1 = rejected (`not-red-at-base` or `not-green-at-result`),
|
||||
// 2 = could not run (`not-run`: no test files, an unknown start point, a command
|
||||
|
|
@ -57,11 +58,11 @@
|
|||
import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { allowedCommand, runCommand, screenCommand } from './criteria-runner.mjs';
|
||||
import { commandEnv } from '../util/command-env.mjs';
|
||||
import { sliceSteps, stepTestsOnly } from '../parsers/plan-schema.mjs';
|
||||
import { parseStepFields, sliceSteps, stepTestFile, stepTestsOnlyFromFields } from '../parsers/plan-schema.mjs';
|
||||
|
||||
export const DEFAULT_TIMEOUT_MS = 900_000;
|
||||
const TAIL = 2000;
|
||||
|
|
@ -110,23 +111,42 @@ const notRun = (reason, extra = {}) => ({ status: 'not-run', reason, ...extra })
|
|||
// the executor sends the step up to the plan instead of into fix rounds
|
||||
// (voyage-lekerepo trial 3, 2026-10-09). `null` = the plan or the step could
|
||||
// not be read; without `--plan` the field is absent and nothing new happens.
|
||||
function testsOnly(o) {
|
||||
//
|
||||
// The plan's `Files:` is prose, read by a reader that can misread it; the
|
||||
// step's own diff is a fact. So a step the plan calls tests-only is so only
|
||||
// when every file it changed since the start point is a test too (release
|
||||
// review of 6.3.0, round 6). Before building (`--red-only`) the diff holds the
|
||||
// step's test alone, so there the plan decides. The plan file itself and
|
||||
// Voyage's own files under .claude/ (progress, approvals) are not the step's change.
|
||||
function testsOnly(o, cwd, baseSha) {
|
||||
if (!o.plan) return {};
|
||||
let text;
|
||||
try { text = readFileSync(o.plan, 'utf8'); }
|
||||
catch (e) { return { tests_only: null, tests_only_note: `cannot read the plan: ${e.message}` }; }
|
||||
const step = sliceSteps(text).find((s) => Number(s.n) === Number(o.step));
|
||||
if (!step) return { tests_only: null, tests_only_note: `no "### Step ${o.step}:" in the plan` };
|
||||
return { tests_only: stepTestsOnly(step.body).testsOnly };
|
||||
const fields = parseStepFields(step.body);
|
||||
if (!stepTestsOnlyFromFields(fields).testsOnly) return { tests_only: false };
|
||||
const isTest = stepTestFile(fields);
|
||||
const given = new Set(o.tests ?? []);
|
||||
const changed = [
|
||||
...lines(git(cwd, ['diff', '--name-only', '--no-renames', baseSha]).stdout),
|
||||
...lines(git(cwd, ['ls-files', '--others', '--exclude-standard']).stdout),
|
||||
];
|
||||
const planRel = relative(cwd, realpathSync(o.plan)).split(sep).join('/');
|
||||
const other = [...new Set(changed)].filter((p) => p !== planRel && !p.startsWith('.claude/') && !given.has(p) && !isTest(p));
|
||||
if (other.length === 0) return { tests_only: true };
|
||||
return { tests_only: false, tests_only_note: `the plan's Files: names only tests, but the step's diff also changes ${other.slice(0, 5).join(', ')}` };
|
||||
}
|
||||
|
||||
// A program that could not start (a spawn error: not installed), a signal (the
|
||||
// timeout), or 126/127 from a script that could not run its own command is not
|
||||
// a test verdict at all.
|
||||
// A program that could not start (a spawn error: not installed) or a signal
|
||||
// (the timeout) is not a test verdict at all. With no shell, an exit 126/127
|
||||
// is the test program's own (a script whose tool the step has not built yet):
|
||||
// a red like any other, and the control run tells an environment fault apart
|
||||
// (release review of 6.3.0, round 6).
|
||||
function unrunnable(r) {
|
||||
if (r.spawnError) return `the command could not start: ${r.spawnError}`;
|
||||
if (r.exit === null) return `the command was killed (${r.signal ?? 'timeout'})`;
|
||||
if (r.exit === 126 || r.exit === 127) return `the command could not be run (exit ${r.exit})`;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +241,7 @@ export function proveRed(o) {
|
|||
const baseBroken = unrunnable(base);
|
||||
if (baseBroken) return notRun(`at the start point: ${baseBroken}`, { base, result, linked });
|
||||
const out = { base_sha: baseSha, linked, base, result };
|
||||
if (base.exit === 0) return { status: 'rejected', reason: 'not-red-at-base', ...testsOnly(o), ...out };
|
||||
if (base.exit === 0) return { status: 'rejected', reason: 'not-red-at-base', ...testsOnly(o, cwd, baseSha), ...out };
|
||||
if (o.redOnly) return { status: 'proven', reason: 'red at the start point (--red-only: the result was not run)', red_kind: redKind(base), base_sha: baseSha, linked, base };
|
||||
if (result.exit !== 0) return { status: 'rejected', reason: 'not-green-at-result', ...out };
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
// 1 = it failed, the executor denylist blocked it, or it is not runnable
|
||||
// (no command, or a command outside the test-runner allowlist): the
|
||||
// step is not done, and that is a finding for the fix rounds
|
||||
// 2 = could not run: bad usage, a plan that cannot be read, no step N
|
||||
// 2 = could not run: bad usage, a plan that cannot be read, no step N, or
|
||||
// a runner that could not start (not installed): nothing was measured,
|
||||
// as red-proof and done-line say for the same case
|
||||
// JSON on stdout: {step, status, progress_status, command, exit_code, output}
|
||||
// status: passed | failed | blocked | unrunnable | absent | error
|
||||
// progress_status: the value progress.json takes for steps.N.regression
|
||||
|
|
@ -55,13 +57,15 @@ export function checkRegression({ planText, step, cwd = process.cwd(), run = (c,
|
|||
if (!field) return withProgress({ ...base, status: 'absent', output: 'no Regression: field — nothing to run', exit: EXIT.absent });
|
||||
const { command, reason } = firstCommand(field.text);
|
||||
const [r] = run([{ label: `R${step}`, text: field.text, command, reason }], { cwd });
|
||||
// A runner that could not start measured nothing: not a red suite (round 6).
|
||||
const status = r.startError ? 'error' : r.status;
|
||||
return withProgress({
|
||||
step,
|
||||
status: r.status,
|
||||
status,
|
||||
command: r.command ?? null,
|
||||
exit_code: r.exitCode ?? null,
|
||||
output: r.output ?? '',
|
||||
exit: EXIT[r.status] ?? EXIT.error,
|
||||
exit: EXIT[status] ?? EXIT.error,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue