FN-8714: fix quality runner CLI argument passthrough
Allow dashboard aggregate quality commands through the pnpm script boundary. - Normalize pnpm’s leading argument separator before validating runner options - Add end-to-end aggregate and fail-fast quality-runner coverage with a deterministic fixture - Document aggregate quality-runner invocation and failure reporting Files changed: docs/testing.md | 15 ++++ packages/dashboard/scripts/__tests__/quality-runner-fixture.mjs | 18 +++++ packages/dashboard/scripts/__tests__/run-quality-tests.test.ts | 88 ++++++++++++++++++++++ packages/dashboard/scripts/run-quality-tests.mjs | 23 ++++-- 4 files changed, 139 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-8714 Fusion-Task-Lineage: 48dbd434-a9e6-4283-8658-cbaa254a7dc7 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -209,6 +209,21 @@ was SIGKILLed by heap pressure under workspace worker budgeting. The top-level
|
||||
`pretest` artifact bootstrap runs once before the orchestrator; lane subprocesses must
|
||||
not re-run `scripts/ensure-test-artifacts.mjs`.
|
||||
|
||||
Use the exact aggregate command below to attempt all 15 app/API lanes, including after
|
||||
one or more lanes fail:
|
||||
|
||||
```bash
|
||||
pnpm --filter @fusion/dashboard test -- --all
|
||||
```
|
||||
|
||||
pnpm forwards that invocation as `-- --all`; the quality runner deliberately removes
|
||||
only the leading package-script separator and still rejects genuine unknown options.
|
||||
`--no-fail-fast` is an equivalent aggregate alias. Without either alias, the default is
|
||||
fail-fast for quick local feedback: remaining lanes are reported as **NOT RUN** with
|
||||
**UNKNOWN** status, never as passing. Aggregate mode attempts every lane exactly once;
|
||||
any failed or signal-terminated lane keeps the command nonzero and is named in the
|
||||
final failure summary. It must never print an all-passed summary when any lane failed.
|
||||
|
||||
<!-- FNXC:TestInfrastructure 2026-06-21-12:21: FN-6854 applies the dashboard heap-runner pattern to the engine affected-package lane because a wide `vitest --changed` fan-out selected hundreds of real-git-heavy engine files and could be OS-SIGKILLed by heap pressure before Vitest returned a verdict. Keep the engine lane isolated, heap-capped, and lower-worker rather than raising concurrency or widening timeouts.
|
||||
|
||||
FNXC:TestInfrastructure 2026-06-21-16:28: FN-6877 applies the same changed-mode envelope to the dashboard scoped affected lane because FN-6874 showed App/jsdom changed runs could be OS-OOM-killed even with inbound test concurrency already set to 1. Keep the per-lane watchdog finite and outside the env; the envelope is a heap-pressure guard, not a hang-budget increase.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { appendFileSync } from "node:fs";
|
||||
import process from "node:process";
|
||||
|
||||
const projectIndex = process.argv.indexOf("--project");
|
||||
const project = process.argv[projectIndex + 1] ?? "unknown-project";
|
||||
|
||||
if (process.env.FUSION_DASHBOARD_QUALITY_LANE_LOG) {
|
||||
appendFileSync(process.env.FUSION_DASHBOARD_QUALITY_LANE_LOG, `${project}\n`);
|
||||
}
|
||||
|
||||
if (process.env.FUSION_DASHBOARD_QUALITY_SIGNAL_PROJECT === project) {
|
||||
process.kill(process.pid, "SIGTERM");
|
||||
}
|
||||
|
||||
const failedProjects = new Set((process.env.FUSION_DASHBOARD_QUALITY_FAIL_PROJECTS ?? "").split(",").filter(Boolean));
|
||||
process.exitCode = failedProjects.has(project) ? 1 : 0;
|
||||
@@ -1,7 +1,16 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const workspaceRoot = resolve(__dirname, "../../../..");
|
||||
const fixtureRunnerPath = join(__dirname, "quality-runner-fixture.mjs");
|
||||
|
||||
interface QualityLane {
|
||||
name: string;
|
||||
group: "app" | "api";
|
||||
@@ -17,6 +26,7 @@ interface LaneResult {
|
||||
|
||||
interface RunQualityTestsModule {
|
||||
qualityLanes: QualityLane[];
|
||||
parseArgs(argv: string[]): { group: "all" | "app" | "api"; list: boolean; allLanes: boolean };
|
||||
resolveConcurrency(env?: Record<string, string | undefined>): number;
|
||||
runQualityTests(options?: {
|
||||
failFast?: boolean;
|
||||
@@ -133,6 +143,84 @@ describe("dashboard quality orchestrator", () => {
|
||||
expect(result).toMatchObject({ ok: false, skipped: 2 });
|
||||
});
|
||||
|
||||
it("parses direct and pnpm-forwarded aggregate aliases while rejecting genuine unknown arguments", async () => {
|
||||
const { parseArgs } = await loadModule();
|
||||
|
||||
expect(parseArgs([])).toEqual({ group: "all", list: false, allLanes: false });
|
||||
expect(parseArgs(["--all"])).toEqual({ group: "all", list: false, allLanes: true });
|
||||
expect(parseArgs(["--no-fail-fast"])).toEqual({ group: "all", list: false, allLanes: true });
|
||||
expect(parseArgs(["--", "--all"])).toEqual({ group: "all", list: false, allLanes: true });
|
||||
expect(parseArgs(["--", "--no-fail-fast"])).toEqual({ group: "all", list: false, allLanes: true });
|
||||
expect(parseArgs(["--group", "app", "--list"])).toEqual({ group: "app", list: true, allLanes: false });
|
||||
expect(() => parseArgs(["--", "--not-a-quality-option"])).toThrow("Unknown argument: --not-a-quality-option");
|
||||
});
|
||||
|
||||
it.each(["--all", "--no-fail-fast"])("runs package aggregate alias %s through all 15 lanes without hiding failures", async (aggregateAlias) => {
|
||||
const { qualityLanes } = await loadModule();
|
||||
const temporaryDirectory = mkdtempSync(join(tmpdir(), "fn-8714-quality-runner-"));
|
||||
const laneLogPath = join(temporaryDirectory, "lanes.log");
|
||||
|
||||
try {
|
||||
const result = spawnSync("pnpm", ["--filter", "@fusion/dashboard", "test", "--", aggregateAlias], {
|
||||
cwd: workspaceRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_ignore_scripts: "true",
|
||||
FUSION_DASHBOARD_TEST_CONCURRENCY: "1",
|
||||
FUSION_DASHBOARD_QUALITY_TEST_MODE: "1",
|
||||
FUSION_DASHBOARD_QUALITY_RUNNER: fixtureRunnerPath,
|
||||
FUSION_DASHBOARD_QUALITY_LANE_LOG: laneLogPath,
|
||||
FUSION_DASHBOARD_QUALITY_FAIL_PROJECTS: "dashboard-app-quality-foundation-api,dashboard-api-quality",
|
||||
},
|
||||
});
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
const startedLanes = [...output.matchAll(/^\[dashboard-quality\] start ([^:]+:[^:]+):/gm)].map((match) => match[1]);
|
||||
const launchedProjects = existsSync(laneLogPath) ? readFileSync(laneLogPath, "utf8").trim().split("\n") : [];
|
||||
|
||||
expect(result.status, output).toBe(1);
|
||||
expect(output).not.toContain("Unknown argument: --");
|
||||
expect(startedLanes).toEqual(qualityLanes.map((qualityLane) => qualityLane.name));
|
||||
expect(new Set(startedLanes).size).toBe(15);
|
||||
expect(launchedProjects).toHaveLength(15);
|
||||
expect(output).toContain("failed lane(s): app:foundation-api, api:curated");
|
||||
expect(output).not.toContain("NOT RUN");
|
||||
expect(output).not.toContain("all 15 lane(s) passed");
|
||||
} finally {
|
||||
rmSync(temporaryDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the no-argument package command fail-fast and labels unrun lanes unknown", async () => {
|
||||
const temporaryDirectory = mkdtempSync(join(tmpdir(), "fn-8714-default-quality-runner-"));
|
||||
const laneLogPath = join(temporaryDirectory, "lanes.log");
|
||||
|
||||
try {
|
||||
const result = spawnSync("pnpm", ["--filter", "@fusion/dashboard", "test"], {
|
||||
cwd: workspaceRoot,
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_ignore_scripts: "true",
|
||||
FUSION_DASHBOARD_TEST_CONCURRENCY: "1",
|
||||
FUSION_DASHBOARD_QUALITY_TEST_MODE: "1",
|
||||
FUSION_DASHBOARD_QUALITY_RUNNER: fixtureRunnerPath,
|
||||
FUSION_DASHBOARD_QUALITY_LANE_LOG: laneLogPath,
|
||||
FUSION_DASHBOARD_QUALITY_FAIL_PROJECTS: "dashboard-app-quality-foundation-api",
|
||||
},
|
||||
});
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
const launchedProjects = readFileSync(laneLogPath, "utf8").trim().split("\n");
|
||||
|
||||
expect(result.status, output).toBe(1);
|
||||
expect(launchedProjects).toEqual(["dashboard-app-quality-foundation-api"]);
|
||||
expect(output).toContain("14 lane(s) were NOT RUN after the first failure — their status is UNKNOWN, not passing.");
|
||||
expect(output).not.toContain("all 1 lane(s) passed");
|
||||
} finally {
|
||||
rmSync(temporaryDirectory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("failFast:false runs EVERY lane and reports every failure, not just the first", async () => {
|
||||
const { runQualityTests } = await loadModule();
|
||||
const launched: string[] = [];
|
||||
|
||||
@@ -8,6 +8,8 @@ const HEAP_MB = 6144;
|
||||
const DEFAULT_CONCURRENCY = 2;
|
||||
const MAX_CONCURRENCY = 2;
|
||||
const VITEST_WRAPPER = "scripts/run-vitest-with-heap.mjs";
|
||||
const TEST_RUNNER_ENV = "FUSION_DASHBOARD_QUALITY_RUNNER";
|
||||
const TEST_MODE_ENV = "FUSION_DASHBOARD_QUALITY_TEST_MODE";
|
||||
const EXCLUDE_BUILD_OUTPUT = ["--exclude", "**/build-output.test.ts"];
|
||||
|
||||
export const qualityLanes = [
|
||||
@@ -79,15 +81,23 @@ export function resolveConcurrency(env = process.env) {
|
||||
return Math.min(requested, MAX_CONCURRENCY);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
/*
|
||||
FNXC:DashboardQualityLanes 2026-08-01-17:15:
|
||||
FN-8714 follows FN-8699/#2784: pnpm forwards package-script arguments as a leading standalone
|
||||
`--`, so remove only that boundary marker before validating the runner's own options. Remaining
|
||||
arguments still fail closed; aggregate mode must not turn unknown options or failed lanes into a
|
||||
false passing quality report.
|
||||
*/
|
||||
export function parseArgs(argv) {
|
||||
const normalizedArgv = argv[0] === "--" ? argv.slice(1) : argv;
|
||||
let group = "all";
|
||||
let list = false;
|
||||
let allLanes = false;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
for (let index = 0; index < normalizedArgv.length; index += 1) {
|
||||
const arg = normalizedArgv[index];
|
||||
if (arg === "--group") {
|
||||
group = argv[index + 1] ?? "";
|
||||
group = normalizedArgv[index + 1] ?? "";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -124,9 +134,12 @@ function formatLaneCommand(lane) {
|
||||
function runLane(lane) {
|
||||
return new Promise((resolve) => {
|
||||
const startedAt = Date.now();
|
||||
// FNXC:DashboardQualityLanes 2026-08-01-17:15: The child-process regression runs the exact pnpm
|
||||
// command with a deterministic lane fixture; production always retains the heap wrapper.
|
||||
const runnerPath = process.env[TEST_MODE_ENV] === "1" ? process.env[TEST_RUNNER_ENV] || VITEST_WRAPPER : VITEST_WRAPPER;
|
||||
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], {
|
||||
const child = spawn(process.execPath, [runnerPath, ...lane.args], {
|
||||
cwd: new URL("..", import.meta.url),
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
|
||||
Reference in New Issue
Block a user