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
222 lines
7.2 KiB
JavaScript
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);
|
|
});
|
|
}
|