The structural half of #2784. I re-measured its 123 failures on current main and **all four reported lanes are green** (143 / 2010 / 1961 / 5149 / 2103 passing). This fixes the reason nobody saw them. ## The mechanism `pnpm --filter @fusion/dashboard test` sets `stopScheduling = true` on the first failing lane, so the rest never run — and there was **no flag to ask for a full pass**. The report said: ``` [dashboard-quality] skipped 9 lane(s) after first failure ``` Nine lanes with **unknown** status and nine **passing** lanes produce the same absence of failure text. That is how 123 failures accumulated behind one red lane, and it is why the original issue could only be written by running all twelve lanes by hand. ## What changes, and what deliberately does not Fail-fast stays the **default** — fast feedback on a broken lane is right, and changing it would slow everyone for a rare case. - `--all` (alias `--no-fail-fast`) runs every lane and reports every failure. - `runQualityTests({ failFast })` so the behaviour is reachable from a test, not just the CLI. - The skip line now states the consequence and the remedy: lanes were **NOT RUN**, status **UNKNOWN rather than passing**, and `--all` shows the full set. ## Both halves pinned A flag nobody can prove works is the same as no flag: | test | asserts | |---|---| | DEFAULT stops after the first failing lane | `launched === ["one"]`, `skipped: 2` | | `failFast:false` runs all three | `launched === ["one","two","three"]`, `failed === [one, three]` | The second is the load-bearing one: **lane three ran even though lane one had already failed**, and both failures are reported rather than only the first. **Anti-vacuity control:** reverting the `if (failFast)` plumbing fails the second test and only it (`1 failed / 5 passed`); restoring passes `6/6`. ## Scope Runner and its tests only. No lane contents, no vitest configs, no CI workflow — CI already invokes lanes individually, so this changes local behaviour and the shared helper, not what CI runs. eslint clean; `check-fnxc-future-dates` exit 0. Suggest #2784 closes on the measured-green half and links here for the structural half, so the mechanism does not close along with the symptom that exposed it. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an option to run all quality-test lanes, even when earlier lanes fail. * Added `--all` and `--no-fail-fast` command-line options. * Quality tests now stop on the first failure by default, with clearer output for skipped lanes. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
235 lines
8.0 KiB
JavaScript
235 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/* global console, process */
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { URL, fileURLToPath } from "node:url";
|
|
|
|
const HEAP_MB = 6144;
|
|
const DEFAULT_CONCURRENCY = 2;
|
|
const MAX_CONCURRENCY = 2;
|
|
const VITEST_WRAPPER = "scripts/run-vitest-with-heap.mjs";
|
|
const EXCLUDE_BUILD_OUTPUT = ["--exclude", "**/build-output.test.ts"];
|
|
|
|
export const qualityLanes = [
|
|
{
|
|
name: "app:foundation-api",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-foundation-api", "--silent=passed-only", "--reporter=dot", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:foundation-ui",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-foundation-ui", "--silent=passed-only", "--reporter=dot", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:foundation-hooks-utils",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-foundation-hooks-utils", "--silent=passed-only", "--reporter=dot", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:components-a",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-components-a", "--silent=passed-only", "--reporter=dot", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:components-b",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-components-b", "--silent=passed-only", "--reporter=dot", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:app",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-app", "--reporter=default", "--silent=passed-only", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:chat",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-chat", "--reporter=default", "--silent=passed-only", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
{
|
|
name: "app:settings",
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-settings", "--reporter=default", "--silent=passed-only", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
...[1, 2, 3, 4].map((shard) => ({
|
|
name: `app:backfill-${shard}`,
|
|
group: "app",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-app-quality-backfill", "--silent=passed-only", "--reporter=dot", `--shard=${shard}/4`],
|
|
})),
|
|
{
|
|
name: "api:curated",
|
|
group: "api",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-api-quality", "--silent=passed-only", "--reporter=dot", ...EXCLUDE_BUILD_OUTPUT],
|
|
},
|
|
...[1, 2].map((shard) => ({
|
|
name: `api:backfill-${shard}`,
|
|
group: "api",
|
|
args: ["--heap=6144", "run", "--project", "dashboard-api-quality-backfill", "--silent=passed-only", "--reporter=dot", `--shard=${shard}/2`],
|
|
})),
|
|
];
|
|
|
|
function parsePositiveInt(value) {
|
|
if (value === undefined || value === "") return null;
|
|
const parsed = Number.parseInt(value, 10);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
}
|
|
|
|
export function resolveConcurrency(env = process.env) {
|
|
const requested = parsePositiveInt(env.FUSION_DASHBOARD_TEST_CONCURRENCY) ?? DEFAULT_CONCURRENCY;
|
|
return Math.min(requested, MAX_CONCURRENCY);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
let group = "all";
|
|
let list = false;
|
|
let allLanes = false;
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--group") {
|
|
group = argv[index + 1] ?? "";
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--all" || arg === "--no-fail-fast") {
|
|
allLanes = true;
|
|
continue;
|
|
}
|
|
if (arg.startsWith("--group=")) {
|
|
group = arg.slice("--group=".length);
|
|
continue;
|
|
}
|
|
if (arg === "--list") {
|
|
list = true;
|
|
continue;
|
|
}
|
|
throw new Error(`Unknown argument: ${arg}`);
|
|
}
|
|
|
|
if (!["all", "app", "api"].includes(group)) {
|
|
throw new Error(`Invalid --group value ${JSON.stringify(group)}; expected all, app, or api`);
|
|
}
|
|
|
|
return { group, list, allLanes };
|
|
}
|
|
|
|
function selectLanes(group) {
|
|
return group === "all" ? qualityLanes : qualityLanes.filter((lane) => lane.group === group);
|
|
}
|
|
|
|
function formatLaneCommand(lane) {
|
|
return `node ${VITEST_WRAPPER} ${lane.args.join(" ")}`;
|
|
}
|
|
|
|
function runLane(lane) {
|
|
return new Promise((resolve) => {
|
|
const startedAt = Date.now();
|
|
console.log(`[dashboard-quality] start ${lane.name}: ${formatLaneCommand(lane)}`);
|
|
// process-supervisor-allowlist: foreground test orchestrator runs bounded child processes and waits for each to finish
|
|
const child = spawn(process.execPath, [VITEST_WRAPPER, ...lane.args], {
|
|
cwd: new URL("..", import.meta.url),
|
|
stdio: "inherit",
|
|
env: process.env,
|
|
});
|
|
|
|
child.on("error", (error) => {
|
|
const durationSeconds = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
console.error(`[dashboard-quality] ${lane.name} failed to launch after ${durationSeconds}s`);
|
|
resolve({ lane, ok: false, error });
|
|
});
|
|
|
|
child.on("close", (code, signal) => {
|
|
const durationSeconds = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
if (code === 0 && signal === null) {
|
|
console.log(`[dashboard-quality] pass ${lane.name} (${durationSeconds}s)`);
|
|
resolve({ lane, ok: true });
|
|
return;
|
|
}
|
|
const suffix = signal ? `signal ${signal}` : `exit ${code ?? 1}`;
|
|
console.error(`[dashboard-quality] fail ${lane.name} (${durationSeconds}s, ${suffix})`);
|
|
resolve({ lane, ok: false, code: code ?? 1, signal });
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function runQualityTests({
|
|
group = "all",
|
|
concurrency = resolveConcurrency(),
|
|
lanes = selectLanes(group),
|
|
runner = runLane,
|
|
failFast = true,
|
|
} = {}) {
|
|
const queue = [...lanes];
|
|
const failed = [];
|
|
let running = 0;
|
|
let completed = 0;
|
|
let stopScheduling = false;
|
|
|
|
console.log(
|
|
`[dashboard-quality] running ${queue.length} lane(s), group=${group}, concurrency=${concurrency}, heap=${HEAP_MB}MiB per lane`,
|
|
);
|
|
|
|
return new Promise((resolve) => {
|
|
const schedule = () => {
|
|
while (!stopScheduling && running < concurrency && queue.length > 0) {
|
|
const lane = queue.shift();
|
|
running += 1;
|
|
void runner(lane).then((result) => {
|
|
running -= 1;
|
|
completed += 1;
|
|
if (!result.ok) {
|
|
failed.push(result);
|
|
/* FNXC:DashboardQualityLanes 2026-07-31-18:05 (u12 — #2784's structural half):
|
|
Fail-fast stays the DEFAULT so a broken lane still gives fast local feedback, but it is
|
|
now switchable. Stopping hid 123 real failures behind one red lane (#2784): nine lanes
|
|
were never run, and "skipped 9 lane(s)" reads exactly like a benign skip. */
|
|
if (failFast) stopScheduling = true;
|
|
}
|
|
if ((queue.length === 0 || stopScheduling) && running === 0) {
|
|
resolve({ ok: failed.length === 0, failed, completed, skipped: queue.length });
|
|
return;
|
|
}
|
|
schedule();
|
|
});
|
|
}
|
|
if (queue.length === 0 && running === 0) {
|
|
resolve({ ok: failed.length === 0, failed, completed, skipped: 0 });
|
|
}
|
|
};
|
|
|
|
schedule();
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const { group, list, allLanes } = parseArgs(process.argv.slice(2));
|
|
const lanes = selectLanes(group);
|
|
|
|
if (list) {
|
|
for (const lane of lanes) {
|
|
console.log(`${lane.name}\t${formatLaneCommand(lane)}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const result = await runQualityTests({ group, lanes, failFast: !allLanes });
|
|
if (!result.ok) {
|
|
console.error(`[dashboard-quality] failed lane(s): ${result.failed.map(({ lane }) => lane.name).join(", ")}`);
|
|
if (result.skipped > 0) {
|
|
console.error(
|
|
`[dashboard-quality] ${result.skipped} lane(s) were NOT RUN after the first failure — their status is UNKNOWN, not passing.`
|
|
+ `\n[dashboard-quality] re-run with --all (or --no-fail-fast) to execute every lane and see the full failure set.`,
|
|
);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
console.log(`[dashboard-quality] all ${result.completed} lane(s) passed`);
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
}
|