Files
fusion/packages/dashboard/scripts/run-quality-tests.mjs
gsxdsm 8f66a41c1c FN-6308: parallelize dashboard quality lanes
Reduce dashboard quality gate wall-clock by orchestrating existing Vitest lanes with bounded concurrency.

- Add a dashboard quality orchestrator that runs app/API lanes through the existing heap wrapper with safe concurrency and failure short-circuiting.
- Route dashboard quality scripts through the orchestrator while preserving compatibility lane scripts and shard boundaries.
- Export project glob metadata and strengthen guards for lane parity, heap usage, and settings route response expectations.
- Document the new dashboard quality orchestration model and measured FN-6308 timing results.

Files changed:
 docs/test-speed-audit-FN-5048.md                   |   8 +-
 docs/testing.md                                    |  35 +++-
 packages/dashboard/package.json                    |  14 +-
 .../scripts/__tests__/run-quality-tests.test.ts    | 110 ++++++++++
 packages/dashboard/scripts/run-quality-tests.mjs   | 221 +++++++++++++++++++++
 .../__tests__/dashboard-test-config-guard.test.ts  | 134 ++++++++++---
 .../src/__tests__/routes-settings.test.ts          |  14 +-
 packages/dashboard/vitest.config.ts                |  49 ++++-
 8 files changed, 540 insertions(+), 45 deletions(-)

Fusion-Task-Id: FN-6308

Fusion-Task-Lineage: 05a465fd-8eef-4c03-baa3-2b7cb024fef0
2026-06-12 18:18:24 -07:00

222 lines
7.2 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;
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.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 };
}
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,
} = {}) {
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);
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 } = 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 });
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] skipped ${result.skipped} lane(s) after first failure`);
}
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);
});
}