FN-6854: isolate engine affected tests
Stabilize changed-test runs by giving the engine affected Vitest lane its own memory envelope. - Split @fusion/engine out from other scoped affected-package Vitest runs. - Apply a capped heap and single-worker settings to the isolated engine lane while preserving watchdog timing. - Cover the partitioning and environment behavior with script tests and document the envelope. Files changed: docs/testing.md | 3 +++ scripts/__tests__/test-changed.test.mjs | 45 +++++++++++++++++++++++++++++++++ scripts/test-changed.mjs | 40 ++++++++++++++++++++++++++--- 3 files changed, 84 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-6854 Fusion-Task-Lineage: 144acf15-4ccb-4974-b06c-99cdcc2814dd
This commit is contained in:
@@ -91,6 +91,9 @@ 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`.
|
||||
|
||||
<!-- 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. -->
|
||||
When `scripts/test-changed.mjs` runs affected-package `vitest --changed` scopes, `@fusion/engine` is split out from other scopable packages and gets a dedicated memory envelope: `NODE_OPTIONS=--max-old-space-size=6144` plus `FUSION_TEST_TOTAL_WORKERS=1`, `FUSION_TEST_CONCURRENCY=1`, and `VITEST_MAX_WORKERS=1`. This mirrors the dashboard heap-managed precedent while preserving the `runWithWatchdog` changed-class wall-clock budget. Re-measure with a wide changed selection (for example a dirty `packages/core/src/index.ts` boundary edit) before changing this envelope; the expected failure mode after the fix is a normal Vitest pass/fail verdict, not raw pnpm `SIGKILL`.
|
||||
|
||||
Concurrency knobs:
|
||||
|
||||
- `FUSION_DASHBOARD_TEST_CONCURRENCY` controls dashboard quality lane process
|
||||
|
||||
@@ -34,8 +34,15 @@ import {
|
||||
buildForwardDependencyMap,
|
||||
collectTransitiveDependencies,
|
||||
computeOwnHash,
|
||||
createEngineScopedAffectedEnv,
|
||||
ENGINE_SCOPED_AFFECTED_HEAP_MB,
|
||||
ENGINE_SCOPED_AFFECTED_PACKAGE,
|
||||
ENGINE_SCOPED_AFFECTED_WORKERS,
|
||||
partitionScopedAffectedPackages,
|
||||
} from "../test-changed.mjs";
|
||||
|
||||
import { deriveBudgetMs } from "../lib/run-vitest-watchdog.mjs";
|
||||
|
||||
import { mkdirSync, writeFileSync, mkdtempSync, rmSync, existsSync, utimesSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -253,6 +260,44 @@ test("buildPackageDirByName: uses canonical workspace dirs instead of package al
|
||||
assert.notEqual(result.get("@fusion/engine"), "engine");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// engine scoped affected memory envelope
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("partitionScopedAffectedPackages: isolates engine from other scoped vitest lanes", () => {
|
||||
const groups = partitionScopedAffectedPackages([
|
||||
"@fusion/core",
|
||||
ENGINE_SCOPED_AFFECTED_PACKAGE,
|
||||
"@fusion/dashboard",
|
||||
]);
|
||||
|
||||
assert.deepEqual(groups, [
|
||||
{ packages: ["@fusion/core", "@fusion/dashboard"], engineMemoryEnvelope: false },
|
||||
{ packages: [ENGINE_SCOPED_AFFECTED_PACKAGE], engineMemoryEnvelope: true },
|
||||
]);
|
||||
});
|
||||
|
||||
test("createEngineScopedAffectedEnv: caps heap, lowers workers, and leaves watchdog budget finite", () => {
|
||||
const env = createEngineScopedAffectedEnv({
|
||||
NODE_OPTIONS: "--trace-warnings",
|
||||
FUSION_TEST_TOTAL_WORKERS: "8",
|
||||
FUSION_TEST_CONCURRENCY: "4",
|
||||
VITEST_MAX_WORKERS: "4",
|
||||
HOME: "/tmp/fusion-home",
|
||||
});
|
||||
|
||||
assert.match(env.NODE_OPTIONS, new RegExp(`--max-old-space-size=${ENGINE_SCOPED_AFFECTED_HEAP_MB}`));
|
||||
assert.match(env.NODE_OPTIONS, /--trace-warnings/);
|
||||
assert.equal(env.FUSION_TEST_TOTAL_WORKERS, ENGINE_SCOPED_AFFECTED_WORKERS);
|
||||
assert.equal(env.FUSION_TEST_CONCURRENCY, ENGINE_SCOPED_AFFECTED_WORKERS);
|
||||
assert.equal(env.VITEST_MAX_WORKERS, ENGINE_SCOPED_AFFECTED_WORKERS);
|
||||
assert.equal(env.HOME, "/tmp/fusion-home");
|
||||
|
||||
const budgetMs = deriveBudgetMs({ klass: "changed" });
|
||||
assert.equal(Number.isFinite(budgetMs), true);
|
||||
assert.equal(budgetMs > 0, true);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// decideExecutionPlan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1311,6 +1311,38 @@ export function packageHasVitestConfig(pkgDir, projectRoot = rootDir) {
|
||||
return VITEST_CONFIG_BASENAMES.some((name) => existsSync(path.join(projectRoot, pkgDir, name)));
|
||||
}
|
||||
|
||||
export const ENGINE_SCOPED_AFFECTED_PACKAGE = "@fusion/engine";
|
||||
export const ENGINE_SCOPED_AFFECTED_HEAP_MB = "6144";
|
||||
export const ENGINE_SCOPED_AFFECTED_WORKERS = "1";
|
||||
|
||||
export function prependNodeOption(currentOptions, option) {
|
||||
return [option, currentOptions || ""].join(" ").trim();
|
||||
}
|
||||
|
||||
export function createEngineScopedAffectedEnv(env = process.env) {
|
||||
/*
|
||||
FNXC:TestInfrastructure 2026-06-21-11:24:
|
||||
The engine affected lane can select hundreds of real-git-heavy files when `vitest --changed` sees a widely imported boundary. Run that scoped lane in its own memory envelope: cap Node old-space like the dashboard heap runner and lower Vitest worker fan-out to one process so the lane returns a real pass/fail verdict instead of an OS OOM SIGKILL. Keep watchdog timing outside this env so hangs still fail through `runWithWatchdog`.
|
||||
*/
|
||||
return {
|
||||
...env,
|
||||
NODE_OPTIONS: prependNodeOption(env.NODE_OPTIONS, `--max-old-space-size=${ENGINE_SCOPED_AFFECTED_HEAP_MB}`),
|
||||
FUSION_TEST_TOTAL_WORKERS: ENGINE_SCOPED_AFFECTED_WORKERS,
|
||||
FUSION_TEST_CONCURRENCY: ENGINE_SCOPED_AFFECTED_WORKERS,
|
||||
VITEST_MAX_WORKERS: ENGINE_SCOPED_AFFECTED_WORKERS,
|
||||
};
|
||||
}
|
||||
|
||||
export function partitionScopedAffectedPackages(packages) {
|
||||
const regularPackages = packages.filter((pkg) => pkg !== ENGINE_SCOPED_AFFECTED_PACKAGE);
|
||||
const groups = [];
|
||||
if (regularPackages.length > 0) groups.push({ packages: regularPackages, engineMemoryEnvelope: false });
|
||||
if (packages.includes(ENGINE_SCOPED_AFFECTED_PACKAGE)) {
|
||||
groups.push({ packages: [ENGINE_SCOPED_AFFECTED_PACKAGE], engineMemoryEnvelope: true });
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
export function normalizeForwardedArgs(argv) {
|
||||
const normalized = [];
|
||||
|
||||
@@ -1524,8 +1556,8 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
: [];
|
||||
const fallbackPkgs = activePackages.filter((pkg) => !scopable.includes(pkg));
|
||||
|
||||
for (const { packages, mode } of [
|
||||
{ packages: scopable, mode: "scoped" },
|
||||
for (const { packages, mode, engineMemoryEnvelope = false } of [
|
||||
...partitionScopedAffectedPackages(scopable).map((group) => ({ ...group, mode: "scoped" })),
|
||||
{ packages: fallbackPkgs, mode: "full" },
|
||||
]) {
|
||||
if (packages.length === 0) continue;
|
||||
@@ -1548,11 +1580,11 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
: [...filterArgs, `--workspace-concurrency=${workspaceConcurrency}`, "test", ...forwardedArgs];
|
||||
console.log(
|
||||
mode === "scoped"
|
||||
? `[test-changed] scoped (vitest --changed) run for: ${packages.join(", ")}`
|
||||
? `[test-changed] scoped (vitest --changed) run for: ${packages.join(", ")}${engineMemoryEnvelope ? " (engine memory envelope)" : ""}`
|
||||
: `[test-changed] full package-suite run for: ${packages.join(", ")} (no vitest config / no base)`,
|
||||
);
|
||||
await runMaybeIsolated("pnpm", commandArgs, {
|
||||
env: isolatedHomeEnv,
|
||||
env: engineMemoryEnvelope ? createEngineScopedAffectedEnv(isolatedHomeEnv) : isolatedHomeEnv,
|
||||
onBeforeAfterCheck: cleanupIsolatedHome,
|
||||
// Scoped runs are proportional to the diff, so the tight "changed" ceiling
|
||||
// applies; a hang fails fast instead of blocking the 60-min full backstop.
|
||||
|
||||
Reference in New Issue
Block a user