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:
gsxdsm
2026-06-21 12:21:45 -07:00
parent ca08828813
commit d57538581d
3 changed files with 84 additions and 4 deletions

View File

@@ -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

View File

@@ -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
// ---------------------------------------------------------------------------

View File

@@ -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.