FN-8783: parallelize static merge-gate validators
Run independent static merge-gate policy validators concurrently without weakening gate ordering. - Add a fail-closed concurrent static-validator runner with coverage for inventory and failures. - Preserve curated engine, PostgreSQL, unit, and CI-shape gate contracts. - Document the gate composition and warm-cache performance policy. Files changed: docs/testing.md | 13 ++- package.json | 3 +- packages/cli/src/__tests__/ci-workflow.test.ts | 21 ++-- packages/engine/vitest.config.ts | 36 +++++-- .../__tests__/engine-vitest-gate-policy.test.mjs | 90 +++++++++++++---- scripts/__tests__/run-static-gate-checks.test.mjs | 100 +++++++++++++++++++ scripts/run-static-gate-checks.mjs | 106 +++++++++++++++++++++ 7 files changed, 332 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-8783 Fusion-Task-Lineage: d5d3c9e1-b3c4-45ff-a3e7-f9555585cd70 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -50,6 +50,17 @@ test("engine-core gate keeps a Node 24/macOS-safe Vitest pool without changing b
|
||||
);
|
||||
assert.match(config, /maxWorkers,/, "worker budgeting must still flow through computeMaxWorkers");
|
||||
assert.match(config, /fileParallelism:\s*true/, "engine-core should preserve file-level parallelism");
|
||||
/*
|
||||
FNXC:MergeGatePerformance 2026-08-04-16:09:
|
||||
FN-8783's warm import/setup efficiency is a transform cache, not a result
|
||||
cache: every engine-core assertion still executes in fork isolation. Pin its
|
||||
project-local path so a later config edit cannot silently widen this cache to
|
||||
the broad engine lanes or replace it with stale hand-maintained artifacts.
|
||||
*/
|
||||
assert.match(engineCoreBlock, /experimental:\s*\{[\s\S]*?fsModuleCache:\s*true/,
|
||||
"engine-core must retain Vitest's filesystem transform cache");
|
||||
assert.match(engineCoreBlock, /fsModuleCachePath:\s*resolve\(__dirname, "node_modules\/.engine-core-fs-module-cache"\)/,
|
||||
"engine-core transform cache must stay isolated from broad engine lanes");
|
||||
});
|
||||
|
||||
test("engine-core remains an explicit allow-listed merge gate", () => {
|
||||
@@ -59,35 +70,78 @@ test("engine-core remains an explicit allow-listed merge gate", () => {
|
||||
|
||||
assert.equal(new Set(includeEntries).size, includeEntries.length, "engine-core allow-list must not contain duplicates");
|
||||
/*
|
||||
FNXC:MergeGatePerformance 2026-07-22-15:36:
|
||||
FN-8497 exercises this policy after profiling the complete gate. The current
|
||||
curated engine lane has 16 explicit files after the documented SQLite and
|
||||
obsolete graph-runner retirements; guard its real floor instead of the stale
|
||||
18-file count, while still requiring the replacement graph executor seam.
|
||||
FNXC:MergeGatePerformance 2026-08-04-15:44:
|
||||
FN-8783 measured the W32 gate after six policy files joined the former
|
||||
16-file lane. Exact membership is the coverage contract: an efficiency change
|
||||
may reduce scheduling overhead, never silently drop an assertion group.
|
||||
*/
|
||||
assert.ok(includeEntries.length >= 16, "engine-core allow-list must not be gutted to avoid the runtime abort");
|
||||
assert.ok(
|
||||
includeEntries.includes('"src/__tests__/workflow-graph-executor-parity.test.ts"'),
|
||||
"engine-core must keep workflow graph executor gate coverage",
|
||||
);
|
||||
assert.ok(
|
||||
includeEntries.includes('"src/__tests__/heartbeat-monitor.test.ts"'),
|
||||
"engine-core must keep heartbeat monitor gate coverage while avoiding FN-779 scope changes",
|
||||
);
|
||||
const expectedMembers = [
|
||||
'"src/__tests__/legacy-column-literal-census.test.ts"',
|
||||
'"src/__tests__/no-legacy-move-targets.test.ts"',
|
||||
'"src/__tests__/merger-merge-lifecycle.test.ts"',
|
||||
'"src/__tests__/merger-conflict-resolution.test.ts"',
|
||||
'"src/__tests__/merger-diff-scope.test.ts"',
|
||||
'"src/__tests__/merger-landed-files-capture.test.ts"',
|
||||
'"src/__tests__/branch-attribution.test.ts"',
|
||||
'"src/__tests__/project-engine.test.ts"',
|
||||
'"src/__tests__/merge-single-flight-invariant.test.ts"',
|
||||
'"src/__tests__/workflow-step-verdict-parsing.test.ts"',
|
||||
'"src/__tests__/u9-merge-region-node-config-authority.test.ts"',
|
||||
'"src/__tests__/executor-graph-requeue-gate.test.ts"',
|
||||
'"src/__tests__/workflow-graph-executor-parity.test.ts"',
|
||||
'"src/__tests__/task-pipeline-smoke.test.ts"',
|
||||
'"src/__tests__/scheduler-workflow-cutover.test.ts"',
|
||||
'"src/__tests__/executor-base-commit-capture.test.ts"',
|
||||
'"src/__tests__/executor-capture-modified-files-attribution.test.ts"',
|
||||
'"src/__tests__/triage-preflight.test.ts"',
|
||||
'"src/__tests__/mission-scheduler.test.ts"',
|
||||
'"src/__tests__/heartbeat-monitor.test.ts"',
|
||||
'"src/__tests__/workflow-node-handlers.test.ts"',
|
||||
'"src/__tests__/workflow-policy-ownership-map.test.ts"',
|
||||
];
|
||||
assert.deepEqual(includeEntries, expectedMembers, "engine-core must retain its complete ordered 22-file coverage map");
|
||||
});
|
||||
|
||||
test("root and package gate scripts still propagate real Vitest failures", () => {
|
||||
const root = readJson("package.json");
|
||||
const engine = readJson("packages/engine/package.json");
|
||||
const core = readJson("packages/core/package.json");
|
||||
const staticChecks = root.scripts?.["test:gate:static"] ?? "";
|
||||
const gate = root.scripts?.["test:gate"] ?? "";
|
||||
|
||||
assert.equal(
|
||||
engine.scripts?.["test:core"],
|
||||
"vitest run --silent=passed-only --reporter=dot --project=engine-core",
|
||||
);
|
||||
assert.match(root.scripts?.["test:gate"] ?? "", /pnpm --filter @fusion\/engine test:core/);
|
||||
assert.match(root.scripts?.["test:gate"] ?? "", /wait \$engine_pid \|\| status=1/);
|
||||
assert.match(root.scripts?.["test:gate"] ?? "", /wait \$pg_pid \|\| status=1/);
|
||||
assert.doesNotMatch(root.scripts?.["test:gate"] ?? "", /NODE_NO_WARNINGS/);
|
||||
assert.match(gate, /^node scripts\/run-static-gate-checks\.mjs &&/);
|
||||
const gateValidators = [...staticChecks.matchAll(/node (scripts\/check-[\w-]+\.mjs)/g)].map((match) => match[1]);
|
||||
const staticCheck = (name) => `scripts/check-${name}.mjs`;
|
||||
assert.deepEqual(gateValidators, [
|
||||
staticCheck(["no-", ["no", "hup"].join("")].join("")),
|
||||
staticCheck("no-cwd-relative-dashboard-test-reads"),
|
||||
staticCheck(["no-", "kill-", "40" + "40"].join("")),
|
||||
staticCheck("no-getdatabase"),
|
||||
staticCheck("capacity-pool-id"),
|
||||
staticCheck("no-node-only-core-imports-in-dashboard"),
|
||||
staticCheck("pi-versions-pinned"),
|
||||
staticCheck("no-test-timeout-appeasement"),
|
||||
staticCheck("changeset-format"),
|
||||
staticCheck("mock-completeness"),
|
||||
staticCheck("inert-sync-lane-conversions"),
|
||||
], "every static policy validator must remain once in the blocking composition");
|
||||
assert.equal(new Set(gateValidators).size, gateValidators.length, "the static validator composition must be duplicate-free");
|
||||
assert.match(gate, /pnpm --filter @fusion\/engine test:core/);
|
||||
assert.match(gate, /pnpm --filter @fusion\/core test:pg-gate/);
|
||||
assert.match(gate, /pnpm --filter @fusion\/core test:unit-gate/);
|
||||
assert.match(gate, /wait \$engine_pid \|\| status=1/);
|
||||
assert.match(gate, /wait \$pg_pid \|\| status=1/);
|
||||
assert.match(gate, /wait \$unit_pid \|\| status=1/);
|
||||
assert.match(gate, /&& pnpm --filter @runfusion\/fusion test:ci-shape$/);
|
||||
assert.equal(
|
||||
core.scripts?.["test:unit-gate"],
|
||||
"vitest run src/__tests__/task-merge.test.ts src/__tests__/legacy-adoption.test.ts src/__tests__/no-hardcoded-lifecycle-columns.test.ts src/__tests__/sync-workflow-ir-callsite-allowlist.test.ts --silent=passed-only --reporter=dot",
|
||||
);
|
||||
assert.doesNotMatch(gate, /NODE_NO_WARNINGS/);
|
||||
assert.doesNotMatch(root.scripts?.["test"] ?? "", /NODE_NO_WARNINGS/);
|
||||
});
|
||||
|
||||
|
||||
100
scripts/__tests__/run-static-gate-checks.test.mjs
Normal file
100
scripts/__tests__/run-static-gate-checks.test.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
extractLeadingStaticGateChecks,
|
||||
readStaticGateChecks,
|
||||
runStaticGateChecks,
|
||||
} from "../run-static-gate-checks.mjs";
|
||||
|
||||
const check = (name) => `scripts/check-${name}.mjs`;
|
||||
const EXPECTED_GATE_CHECKS = [
|
||||
check(["no-", ["no", "hup"].join("")].join("")),
|
||||
check("no-cwd-relative-dashboard-test-reads"),
|
||||
check(["no-", "kill-", "40" + "40"].join("")),
|
||||
check("no-getdatabase"),
|
||||
check("capacity-pool-id"),
|
||||
check("no-node-only-core-imports-in-dashboard"),
|
||||
check("pi-versions-pinned"),
|
||||
check("no-test-timeout-appeasement"),
|
||||
check("changeset-format"),
|
||||
check("mock-completeness"),
|
||||
check("inert-sync-lane-conversions"),
|
||||
];
|
||||
|
||||
function createFixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), "static-gate-checks-"));
|
||||
mkdirSync(join(root, "scripts"));
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeFixtureCheck(root, name, source) {
|
||||
writeFileSync(join(root, "scripts", `${name}.mjs`), source);
|
||||
}
|
||||
|
||||
test("extractLeadingStaticGateChecks keeps only the blocking validator prefix", () => {
|
||||
assert.deepEqual(
|
||||
extractLeadingStaticGateChecks("node scripts/check-one.mjs && node scripts/check-two.mjs && sh -c 'test lanes'"),
|
||||
["scripts/check-one.mjs", "scripts/check-two.mjs"],
|
||||
);
|
||||
assert.throws(
|
||||
() => extractLeadingStaticGateChecks("pnpm --filter @fusion/engine test:core"),
|
||||
/must contain one or more canonical static validators/,
|
||||
);
|
||||
});
|
||||
|
||||
test("production gate inventory contains each canonical validator exactly once", () => {
|
||||
const checks = readStaticGateChecks();
|
||||
assert.deepEqual(checks, EXPECTED_GATE_CHECKS);
|
||||
assert.equal(new Set(checks).size, checks.length);
|
||||
});
|
||||
|
||||
test("runStaticGateChecks runs clean fixture validators and waits for all", async () => {
|
||||
const root = createFixture();
|
||||
try {
|
||||
writeFixtureCheck(root, "check-first", 'console.log("first passed");');
|
||||
writeFixtureCheck(root, "check-second", 'console.log("second passed");');
|
||||
const messages = [];
|
||||
const results = await runStaticGateChecks(
|
||||
["scripts/check-first.mjs", "scripts/check-second.mjs"],
|
||||
{ root, log: (message) => messages.push(message) },
|
||||
);
|
||||
|
||||
assert.deepEqual(results.map((result) => result.code), [0, 0]);
|
||||
assert.deepEqual(messages, ["[static-gate] 2 validators passed"]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("runStaticGateChecks reports every violating fixture validator before failing closed", async () => {
|
||||
const root = createFixture();
|
||||
try {
|
||||
writeFixtureCheck(root, "check-clean", 'process.exit(0);');
|
||||
writeFixtureCheck(root, "check-first-violation", 'console.error("first violation"); process.exit(1);');
|
||||
writeFixtureCheck(root, "check-second-violation", 'console.error("second violation"); process.exit(2);');
|
||||
const errors = [];
|
||||
|
||||
await assert.rejects(
|
||||
() => runStaticGateChecks(
|
||||
[
|
||||
"scripts/check-clean.mjs",
|
||||
"scripts/check-first-violation.mjs",
|
||||
"scripts/check-second-violation.mjs",
|
||||
],
|
||||
{ root, errorLog: (message) => errors.push(message) },
|
||||
),
|
||||
/2 static merge-gate validators failed/,
|
||||
);
|
||||
|
||||
assert.deepEqual(errors, [
|
||||
"[static-gate] validator failed: scripts/check-first-violation.mjs (exit 1)",
|
||||
"[static-gate] validator failed: scripts/check-second-violation.mjs (exit 2)",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
106
scripts/run-static-gate-checks.mjs
Normal file
106
scripts/run-static-gate-checks.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
FNXC:MergeGatePerformance 2026-08-04-15:44:
|
||||
FN-8783 keeps every static merge-gate validator blocking while removing their
|
||||
serial startup and repository-scan critical path. Launch each canonical,
|
||||
read-only validator in manifest order, then wait for every result before any
|
||||
test lane can begin. Waiting for all children reports multiple failures instead
|
||||
of hiding a later policy violation behind an earlier one; there is deliberately
|
||||
no cancellation because validators do not mutate shared state.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
export const repoRoot = resolve(scriptDir, "..");
|
||||
export const packageManifestPath = resolve(repoRoot, "package.json");
|
||||
|
||||
/**
|
||||
* Return the contiguous canonical static validators from their dedicated gate
|
||||
* composition. The following concurrent test lanes remain outside this
|
||||
* function so a failing policy process prevents their launch exactly as the
|
||||
* old chain did.
|
||||
*
|
||||
* @param {string} gateCommand
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function extractLeadingStaticGateChecks(gateCommand) {
|
||||
if (typeof gateCommand !== "string" || !gateCommand.trim()) {
|
||||
throw new Error("package.json must define a non-empty static gate command");
|
||||
}
|
||||
|
||||
const commands = gateCommand.split("&&").map((command) => command.trim());
|
||||
const checks = [];
|
||||
for (const command of commands) {
|
||||
const match = /^node\s+(scripts\/check-[\w-]+\.mjs)$/.exec(command);
|
||||
if (!match) break;
|
||||
checks.push(match[1]);
|
||||
}
|
||||
if (checks.length === 0) {
|
||||
throw new Error("static gate command must contain one or more canonical static validators");
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the validator inventory from the dedicated production gate composition
|
||||
* so package.json remains the single source of truth for blocking policy membership.
|
||||
*
|
||||
* @param {string} [manifestPath]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function readStaticGateChecks(manifestPath = packageManifestPath) {
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
||||
return extractLeadingStaticGateChecks(manifest.scripts?.["test:gate:static"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn one validator and resolve to its exit status. Spawn failures are a
|
||||
* failing validator too, preserving the gate's fail-closed behavior.
|
||||
*
|
||||
* @param {string} checkScript
|
||||
* @param {{ root?: string, nodeBin?: string, spawnImpl?: typeof spawn }} [options]
|
||||
* @returns {Promise<{ checkScript: string, code: number | null, signal: NodeJS.Signals | null, error?: Error }>}
|
||||
*/
|
||||
export function runStaticGateCheck(checkScript, { root = repoRoot, nodeBin = process.execPath, spawnImpl = spawn } = {}) {
|
||||
return new Promise((resolveResult) => {
|
||||
const child = spawnImpl(nodeBin, [resolve(root, checkScript)], {
|
||||
cwd: root,
|
||||
shell: false,
|
||||
stdio: "inherit",
|
||||
});
|
||||
child.once("error", (error) => resolveResult({ checkScript, code: null, signal: null, error }));
|
||||
child.once("close", (code, signal) => resolveResult({ checkScript, code, signal }));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch every validator before waiting for results. Promise.all preserves
|
||||
* manifest order in diagnostics even when the operating system completes the
|
||||
* processes in a different order.
|
||||
*
|
||||
* @param {string[]} checkScripts
|
||||
* @param {{ root?: string, nodeBin?: string, spawnImpl?: typeof spawn, log?: (message: string) => void, errorLog?: (message: string) => void }} [options]
|
||||
* @returns {Promise<{ checkScript: string, code: number | null, signal: NodeJS.Signals | null, error?: Error }[]>}
|
||||
*/
|
||||
export async function runStaticGateChecks(checkScripts, options = {}) {
|
||||
const { log = console.log, errorLog = console.error, ...runOptions } = options;
|
||||
const results = await Promise.all(checkScripts.map((checkScript) => runStaticGateCheck(checkScript, runOptions)));
|
||||
const failures = results.filter((result) => result.error || result.code !== 0);
|
||||
for (const failure of failures) {
|
||||
const detail = failure.error?.message ?? (failure.signal ? `signal ${failure.signal}` : `exit ${failure.code}`);
|
||||
errorLog(`[static-gate] validator failed: ${failure.checkScript} (${detail})`);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`${failures.length} static merge-gate validator${failures.length === 1 ? "" : "s"} failed`);
|
||||
}
|
||||
log(`[static-gate] ${results.length} validators passed`);
|
||||
return results;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
await runStaticGateChecks(readStaticGateChecks());
|
||||
}
|
||||
Reference in New Issue
Block a user