A suite run in which no test passed exits non-zero and names its skips (issue 693)
mesh/merge-gate head 8635560993aa merged into trunk f9be71b9ad44 — pass: the change touches no module of the mesh's graph
mesh/repo-check head 8635560993aa merged into trunk f9be71b9ad44 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery-group group fix/693-a-run-that-proved-nothing-is-not-passed ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request

The runner exits 0 when every test skipped, so a bed that stopped before its
subject and skipped its only test as NOT REACHED was recorded as passed, "0
passed, 0 failed". The suite now collects each skip and its reason through a
reporter of its own, prints NOT PASSED and a skipped line per skip after its
recorded line, keeps them in the receipt, and exits 1 when no test passed or a
test was not reached. A skip for a declared reason beside passes stays passed.
This commit is contained in:
jochen
2026-10-14 03:14:05 +02:00
parent f9be71b9ad
commit 8635560993
5 changed files with 345 additions and 21 deletions
+144
View File
@@ -0,0 +1,144 @@
// A run in which no test passed has proven nothing, and says so (novox/hq issue 693).
//
// Seen on the lab: the genesis bed stopped before its subject, skipped its only test as "NOT REACHED",
// and the suite printed "0 passed, 0 failed" and exited 0, so the lab recorded the run as passed.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { judge, type Receipt, endToEnd } from "../src/lastrun.ts";
import { judgeRun, oneLine, type Skip } from "../src/outcomes.ts";
import { counted, exitOf, reportOn } from "../src/suite.ts";
// The suite as a person runs it: not as a child of this test runner, which would have it report to us.
const suiteEnv = (state: string): NodeJS.ProcessEnv => {
const env: NodeJS.ProcessEnv = { ...process.env, XDG_STATE_HOME: state, NO_COLOR: "1" };
delete env["NODE_TEST_CONTEXT"];
return env;
};
const lab = fileURLToPath(new URL("..", import.meta.url));
const unreached: Skip = {
file: "test/integration/genesis-keeps-its-bus.test.ts",
test: "genesis adopts its bus",
reason: "ADR 0333 §5 NOT REACHED: the first run stopped at step 16 of 24, base, before the broker step:\n docker exited 1",
};
const noBed: Skip = { file: "test/integration/mesh.test.ts", test: "a builder builds", reason: "set MESH_LAB_BUILDER to a built mesh-builder" };
test("a run whose only test skipped did not pass", () => {
const said = judgeRun({ passed: 0, failed: 0 }, [unreached]);
assert.equal(said.passed, false);
assert.match(said.why, /no test passed: 1 skipped, 1 of them as NOT REACHED/);
});
test("a run that ran no test at all did not pass", () => {
assert.deepEqual(judgeRun({ passed: 0, failed: 0 }, []), { passed: false, why: "no test passed: none ran" });
});
test("a NOT REACHED skip beside tests that passed is still not passed", () => {
const said = judgeRun({ passed: 3, failed: 0 }, [unreached]);
assert.equal(said.passed, false);
assert.match(said.why, /1 test\(s\) skipped as NOT REACHED/);
});
// The design limit: a skip its file declares expected, beside tests that passed, leaves the run passed.
test("an expected skip beside tests that passed leaves the run passed", () => {
assert.deepEqual(judgeRun({ passed: 21, failed: 0 }, [noBed]), { passed: true, why: "" });
});
test("the suite's exit is not 0 for a run that did not pass, and is the runner's otherwise", () => {
assert.equal(exitOf(0, { passed: 0, failed: 0 }, [unreached]), 1);
assert.equal(exitOf(0, { passed: 21, failed: 0 }, [noBed]), 0);
assert.equal(exitOf(1, { passed: 20, failed: 1 }, []), 1);
assert.equal(exitOf(0, { passed: null, failed: null }, []), 0, "unread totals are reportOn's case, not this one");
});
test("the recorded line says what skipped and why, and NOT PASSED", () => {
let kept: unknown;
const said = reportOn({ passed: 0, failed: 0, skipped: 1 }, [unreached], (_p, _f, outcome) => {
kept = outcome;
return { against: { "mesh-lab": "7ed7da1" }, ran: [unreached.file] };
});
assert.match(said, /— 0 passed, 0 failed, 1 skipped, against mesh-lab 7ed7da1/);
assert.match(said, /^NOT PASSED: no test passed: 1 skipped, 1 of them as NOT REACHED$/m);
assert.match(said, /^skipped: genesis adopts its bus \(test\/integration\/genesis-keeps-its-bus\.test\.ts\) # ADR 0333 §5 NOT REACHED: the first run stopped at step 16 of 24, base, before the broker step: docker exited 1$/m);
assert.deepEqual(kept, {
skipped: 1,
skips: [{ ...unreached, reason: oneLine(unreached.reason) }],
notPassed: "no test passed: 1 skipped, 1 of them as NOT REACHED",
});
});
test("the runner's skipped total is read", () => {
assert.deepEqual(counted("ℹ tests 1\nℹ pass 0\nℹ fail 0\nℹ skipped 1\n"), { passed: 0, failed: 0, skipped: 1 });
});
test("a receipt of a run that proved nothing is not current, and says why", () => {
const receipt: Receipt = {
at: "2026-10-14T00:33:11Z", passed: 0, failed: 0, against: { "mesh-lab": "aaa" }, ran: [endToEnd],
skipped: 1, skips: [unreached], notPassed: "no test passed: 1 skipped",
};
const said = judge(receipt, new Date("2026-10-14T01:00:00Z"), { "mesh-lab": "aaa" });
assert.equal(said.current, false, said.lines.join("\n"));
const text = said.lines.join("\n");
assert.match(text, /did not pass: no test passed: 1 skipped/);
assert.match(text, /skipped: genesis adopts its bus — ADR 0333 §5 NOT REACHED/);
// An older receipt, from before the fields, with nothing passed, is judged by its count.
const older = judge({ at: receipt.at, passed: 0, failed: 0, against: receipt.against, ran: [endToEnd] },
new Date("2026-10-14T01:00:00Z"), { "mesh-lab": "aaa" });
assert.equal(older.current, false);
});
// The real runner, end to end: the suite command against a file whose only test skips as NOT REACHED,
// the way the genesis bed does.
test("the suite exits non-zero for a file whose only test skipped as NOT REACHED, and records why", () => {
const dir = mkdtempSync(join(tmpdir(), "mesh-lab-693-"));
try {
const file = join(dir, "unreached.test.ts");
writeFileSync(file, [
`import { test } from "node:test";`,
`test("genesis adopts its bus", (t) => {`,
` console.log("a line the bed printed");`,
` t.skip("ADR 0333 §5 NOT REACHED: the first run stopped at step 16 of 24, base:\\nNotFound: content digest");`,
`});`,
].join("\n"));
const ran = spawnSync(process.execPath, ["--experimental-strip-types", join(lab, "src", "cli.ts"), "suite", "--no-build", file], {
cwd: lab, encoding: "utf8", env: suiteEnv(dir),
});
assert.equal(ran.status, 1, `the suite exited ${ran.status}:\n${ran.stdout}${ran.stderr}`);
assert.match(ran.stdout, /0 passed, 0 failed, 1 skipped/);
assert.match(ran.stdout, /^NOT PASSED: no test passed: 1 skipped, 1 of them as NOT REACHED$/m);
assert.match(ran.stdout, /^skipped: genesis adopts its bus \(.*unreached\.test\.ts\) # ADR 0333 §5 NOT REACHED: the first run stopped at step 16 of 24, base: NotFound: content digest$/m);
const receipt = JSON.parse(readFileSync(join(dir, "mesh-lab", "last-run.json"), "utf8")) as Receipt;
assert.equal(receipt.skipped, 1);
assert.equal(receipt.notPassed, "no test passed: 1 skipped, 1 of them as NOT REACHED");
assert.match(receipt.skips?.[0]?.reason ?? "", /NOT REACHED/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("the suite still exits 0 for a file that passes a test and skips another for a declared reason", () => {
const dir = mkdtempSync(join(tmpdir(), "mesh-lab-693-"));
try {
const file = join(dir, "declared.test.ts");
writeFileSync(file, [
`import { test } from "node:test";`,
`test("a module lands", () => {});`,
`test("a builder builds", { skip: "set MESH_LAB_BUILDER to a built mesh-builder" }, () => {});`,
].join("\n"));
const ran = spawnSync(process.execPath, ["--experimental-strip-types", join(lab, "src", "cli.ts"), "suite", "--no-build", file], {
cwd: lab, encoding: "utf8", env: suiteEnv(dir),
});
assert.equal(ran.status, 0, `the suite exited ${ran.status}:\n${ran.stdout}${ran.stderr}`);
assert.match(ran.stdout, /1 passed, 0 failed, 1 skipped/);
assert.doesNotMatch(ran.stdout, /NOT PASSED/);
assert.match(ran.stdout, /^skipped: a builder builds \(.*declared\.test\.ts\) # set MESH_LAB_BUILDER to a built mesh-builder$/m);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
+7 -6
View File
@@ -5,7 +5,7 @@ import { counted, reportOn } from "../src/suite.ts";
// The totals come from the runner's own summary, and from nothing else.
test("the runner's totals are read from its summary", () => {
const said = counted("✔ something (1ms)\nℹ tests 22\nℹ pass 22\nℹ fail 0\n");
assert.deepEqual(said, { passed: 22, failed: 0 });
assert.deepEqual(said, { passed: 22, failed: 0, skipped: null });
});
// A test *named* like a total must not be mistaken for one. The summary is a line of its own, and
@@ -18,7 +18,7 @@ test("a test named like a total is not a total", () => {
// A failing run is read as a failing run.
test("failures are read", () => {
const said = counted("ℹ pass 21\nℹ fail 1\n");
assert.deepEqual(said, { passed: 21, failed: 1 });
assert.deepEqual(said, { passed: 21, failed: 1, skipped: null });
});
// **No totals is not zero failures.** A run whose result could not be read is a run nobody can say
@@ -36,7 +36,7 @@ test("output with no summary yields no totals rather than a clean bill", () => {
// check it — 04-ISSUES/005 in miniature, inside the fix for it.
test("a run whose result could not be read writes nothing", () => {
let wrote = false;
const said = reportOn({ passed: null, failed: null }, () => {
const said = reportOn({ passed: null, failed: null }, [], () => {
wrote = true;
return { against: {}, ran: [] };
});
@@ -46,12 +46,13 @@ test("a run whose result could not be read writes nothing", () => {
test("a run that was read is recorded, with what it was read against", () => {
let got: [number, number] | null = null;
const said = reportOn({ passed: 22, failed: 0 }, (p, f) => {
const said = reportOn({ passed: 22, failed: 0 }, [], (p, f) => {
got = [p, f];
return { against: { "mesh-lab": "abc1234" }, ran: ["test/integration/mesh.test.ts"] };
});
assert.deepEqual(got, [22, 0]);
assert.match(said, /mesh\.test\.ts — 22 passed, 0 failed, against mesh-lab abc1234/);
assert.match(said, /mesh\.test\.ts — 22 passed, 0 failed, 0 skipped, against mesh-lab abc1234/);
assert.doesNotMatch(said, /NOT PASSED/);
});
// The runner's real output, colours and all.
@@ -64,5 +65,5 @@ test("the runner's totals are read from output as it actually arrives", () => {
"\u001b[34m\u2139 pass 22\u001b[39m\n" +
"\u001b[34m\u2139 fail 0\u001b[39m\n" +
"\u001b[34m\u2139 duration_ms 98.9\u001b[39m\n";
assert.deepEqual(counted(real), { passed: 22, failed: 0 });
assert.deepEqual(counted(real), { passed: 22, failed: 0, skipped: null });
});