feat(test): pnpm test runs gate + affected set; remove implicit full-suite escalation (the local OOM path)

- decideExecutionPlan: implicit wide-blast reasons (missing base, diff failed,
  no changes, shared infra, unmapped package) route to new gate mode instead of full
- CI===true force-full branch removed (CI no longer calls test-changed.mjs)
- changed mode runs pnpm test:gate before the affected set
- full suite reachable only via explicit --full / FUSION_TEST_FULL=1
- characterization tests updated + new only-explicit-full invariant test
This commit is contained in:
gsxdsm
2026-06-05 09:08:20 -07:00
parent bbeb86e8f9
commit 5a4bb9eeeb
4 changed files with 101 additions and 48 deletions

View File

@@ -24,7 +24,7 @@ describe("root test command changed-only planning", () => {
expect(plan).toEqual({ mode: "changed", packages: ["@fusion/core", "@fusion/engine"] });
});
it("falls back to full suite when shared test infra changes", () => {
it("routes to gate mode when shared test infra changes (no implicit full suite)", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: "abc123",
@@ -32,10 +32,10 @@ describe("root test command changed-only planning", () => {
packageNameByDir: new Map([["packages/core", "@fusion/core"]]),
});
expect(plan).toEqual({ mode: "full", reason: "shared-infra-changed" });
expect(plan).toEqual({ mode: "gate", reason: "shared-infra-changed" });
});
it("falls back to full suite when comparison base cannot be resolved", () => {
it("routes to gate mode when comparison base cannot be resolved", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: null,
@@ -43,16 +43,16 @@ describe("root test command changed-only planning", () => {
packageNameByDir: new Map(),
});
expect(plan).toEqual({ mode: "full", reason: "missing-comparison-base" });
expect(plan).toEqual({ mode: "gate", reason: "missing-comparison-base" });
});
it("treats unknown package directories as full-suite fallback", () => {
it("treats unknown package directories as gate-mode fallback (resolver returns null)", () => {
const resolved = resolveAffectedPackages(["packages/unknown/src/index.ts"], new Map());
expect(resolved).toBeNull();
});
it("marks root workflow/config changes as full-suite triggers", () => {
expect(shouldForceFullSuite([".github/workflows/ci.yml"])).toBe(true);
it("marks root workflow/config changes as shared-infra (gate-mode) triggers", () => {
expect(shouldForceFullSuite([".github/workflows/pr-checks.yml"])).toBe(true);
expect(shouldForceFullSuite(["package.json"])).toBe(true);
expect(shouldForceFullSuite(["packages/core/src/store.ts"])).toBe(false);
});

View File

@@ -248,47 +248,47 @@ test("decideExecutionPlan: forced full suite", () => {
assert.equal(plan.reason, "forced");
});
test("decideExecutionPlan: missing comparison base → full", () => {
test("decideExecutionPlan: missing comparison base → gate", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: null,
changedFiles: null,
packageNameByDir: basePackageMap,
});
assert.equal(plan.mode, "full");
assert.equal(plan.mode, "gate");
assert.equal(plan.reason, "missing-comparison-base");
});
test("decideExecutionPlan: diff failed → full", () => {
test("decideExecutionPlan: diff failed → gate", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: "abc123",
changedFiles: null,
packageNameByDir: basePackageMap,
});
assert.equal(plan.mode, "full");
assert.equal(plan.mode, "gate");
assert.equal(plan.reason, "diff-failed");
});
test("decideExecutionPlan: no changes → full", () => {
test("decideExecutionPlan: no changes → gate", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: "abc123",
changedFiles: [],
packageNameByDir: basePackageMap,
});
assert.equal(plan.mode, "full");
assert.equal(plan.mode, "gate");
assert.equal(plan.reason, "no-changes");
});
test("decideExecutionPlan: shared infra changed → full", () => {
test("decideExecutionPlan: shared infra changed → gate", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: "abc123",
changedFiles: ["pnpm-lock.yaml"],
packageNameByDir: basePackageMap,
});
assert.equal(plan.mode, "full");
assert.equal(plan.mode, "gate");
assert.equal(plan.reason, "shared-infra-changed");
});
@@ -344,14 +344,14 @@ test("decideExecutionPlan: expands changed packages with reverse dependents", ()
assert.deepEqual(plan.packages, ["@fusion/core", "@fusion/engine", "@fusion/dashboard"]);
});
test("decideExecutionPlan: no affected package resolved → full", () => {
test("decideExecutionPlan: no affected package resolved → gate", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: "abc123",
changedFiles: ["packages/nonexistent/src/foo.ts"],
packageNameByDir: basePackageMap,
});
assert.equal(plan.mode, "full");
assert.equal(plan.mode, "gate");
assert.equal(plan.reason, "no-affected-package");
});
@@ -370,7 +370,7 @@ test("decideExecutionPlan: plugin-only workspace changes stay in changed mode",
assert.deepEqual(plan.packages, ["@fusion-plugin-examples/openclaw-runtime"]);
});
test("decideExecutionPlan: plugin changes without mapping fail safe to full", () => {
test("decideExecutionPlan: plugin changes without mapping fail safe to gate", () => {
const plan = decideExecutionPlan({
forceFullSuite: false,
comparisonBase: "abc123",
@@ -378,7 +378,7 @@ test("decideExecutionPlan: plugin changes without mapping fail safe to full", ()
packageNameByDir: basePackageMap,
});
assert.equal(plan.mode, "full");
assert.equal(plan.mode, "gate");
assert.equal(plan.reason, "no-affected-package");
});
@@ -857,25 +857,41 @@ test("emitModeDecision: changed plan reports changed-packages reason + package c
assert.deepEqual(lines, [line]);
});
test("emitModeDecision: full plan surfaces the decideExecutionPlan reason, packages=0", () => {
test("emitModeDecision: gate plan surfaces the decideExecutionPlan reason, packages=0", () => {
assert.equal(
emitModeDecision({ mode: "full", reason: "missing-comparison-base" }, () => {}),
"[test-changed] mode=full reason=missing-comparison-base packages=0",
emitModeDecision({ mode: "gate", reason: "missing-comparison-base" }, () => {}),
"[test-changed] mode=gate reason=missing-comparison-base packages=0",
);
assert.equal(
emitModeDecision({ mode: "full", reason: "shared-infra-changed" }, () => {}),
"[test-changed] mode=full reason=shared-infra-changed packages=0",
emitModeDecision({ mode: "gate", reason: "shared-infra-changed" }, () => {}),
"[test-changed] mode=gate reason=shared-infra-changed packages=0",
);
});
test("emitModeDecision: distinct full reasons round-trip from decideExecutionPlan", () => {
const full = decideExecutionPlan({ forceFullSuite: false, comparisonBase: null });
assert.equal(emitModeDecision(full, () => {}), "[test-changed] mode=full reason=missing-comparison-base packages=0");
test("emitModeDecision: gate and forced-full reasons round-trip from decideExecutionPlan", () => {
const gate = decideExecutionPlan({ forceFullSuite: false, comparisonBase: null });
assert.equal(emitModeDecision(gate, () => {}), "[test-changed] mode=gate reason=missing-comparison-base packages=0");
const forced = decideExecutionPlan({ forceFullSuite: true });
assert.equal(emitModeDecision(forced, () => {}), "[test-changed] mode=full reason=forced packages=0");
});
// The implicit full-suite escalation was the local OOM path (FN: merge-gate
// redesign). The full suite must be reachable ONLY via explicit opt-in.
test("decideExecutionPlan: full mode is reachable only via forceFullSuite", () => {
const implicitInputs = [
{ forceFullSuite: false, comparisonBase: null },
{ forceFullSuite: false, comparisonBase: "origin/main", changedFiles: null },
{ forceFullSuite: false, comparisonBase: "origin/main", changedFiles: [] },
{ forceFullSuite: false, comparisonBase: "origin/main", changedFiles: [".github/workflows/pr-checks.yml"] },
{ forceFullSuite: false, comparisonBase: "origin/main", changedFiles: ["unmapped/path.ts"], packageNameByDir: new Map() },
];
for (const input of implicitInputs) {
const plan = decideExecutionPlan(input);
assert.equal(plan.mode, "gate", `expected gate mode for ${JSON.stringify(input)}`);
}
});
// ---------------------------------------------------------------------------
// U3: cache-fresh fast path — when every changed package is cache-fresh,
// applyCacheToPlan yields zero active packages, which is the signal that lets

View File

@@ -9,6 +9,9 @@
* 3. The server shuts down cleanly on SIGTERM.
*
* Safety properties (see scripts/check-no-kill-4040.mjs and AGENTS.md):
* (port-4040-allowlist: this file only ever AVOIDS the reserved ports — it
* requests an ephemeral port and rejects reserved ones; it never binds,
* probes, or kills them.)
* - Never binds or touches port 4040 / FUSION_RESERVED_PORTS — an ephemeral
* port is requested from the OS (listen on 0) and double-checked against
* the reserved list.

View File

@@ -430,8 +430,16 @@ function isTestIrrelevantRootPath(file) {
export function shouldForceFullSuite(changedFiles) {
// NOTE: overlaps SHARED_HASH_INPUT_PATHS by intent (different axis: this list
// forces full-suite mode; that one busts every package's cache hash). When
// adding a new shared root config input, consider both lists.
// signals shared-infra changes; that one busts every package's cache hash).
// When adding a new shared root config input, consider both lists.
//
// HISTORY: this signal used to escalate `pnpm test` to an implicit full
// recursive run — which was the local OOM path (two concurrent heavy
// packages, 6GB dashboard heaps). Since the merge-gate redesign
// (docs/plans/2026-06-04-001-refactor-fast-trusted-test-gate-plan.md) it
// routes to GATE mode instead: run the merge-gate suite and point at
// `pnpm test:full` for the explicit full sweep. The full suite only ever
// runs on explicit opt-in (--full / FUSION_TEST_FULL=1).
const fullSuitePaths = [
"package.json",
"pnpm-lock.yaml",
@@ -1012,13 +1020,16 @@ export function decideExecutionPlan({
reverseDependencyMap,
}) {
if (forceFullSuite) return { mode: "full", reason: "forced" };
if (!comparisonBase) return { mode: "full", reason: "missing-comparison-base" };
if (!changedFiles) return { mode: "full", reason: "diff-failed" };
if (changedFiles.length === 0) return { mode: "full", reason: "no-changes" };
if (shouldForceFullSuite(changedFiles)) return { mode: "full", reason: "shared-infra-changed" };
// Every implicit wide-blast condition below routes to GATE mode (merge-gate
// suite only), never to an implicit full-suite run — the old escalation was
// the local OOM path. `pnpm test:full` is the explicit opt-in full sweep.
if (!comparisonBase) return { mode: "gate", reason: "missing-comparison-base" };
if (!changedFiles) return { mode: "gate", reason: "diff-failed" };
if (changedFiles.length === 0) return { mode: "gate", reason: "no-changes" };
if (shouldForceFullSuite(changedFiles)) return { mode: "gate", reason: "shared-infra-changed" };
const affectedPackages = resolveAffectedPackages(changedFiles, packageNameByDir);
if (!affectedPackages || affectedPackages.length === 0) return { mode: "full", reason: "no-affected-package" };
if (!affectedPackages || affectedPackages.length === 0) return { mode: "gate", reason: "no-affected-package" };
return {
mode: "changed",
@@ -1060,8 +1071,11 @@ export function normalizeForwardedArgs(argv) {
}
export function main(argv = process.argv.slice(2)) {
// The full suite is explicit opt-in ONLY (--full / FUSION_TEST_FULL=1).
// CI no longer routes through this script (the gate job runs `pnpm
// test:gate`; the demoted tier runs `test:ci:shard` in full-suite.yml), so
// the old `CI === "true"` force-full branch is gone.
const forceFullSuite =
process.env.CI === "true" ||
process.env.FUSION_TEST_FULL === "1" ||
argv.includes("--full");
@@ -1136,7 +1150,10 @@ export function main(argv = process.argv.slice(2)) {
}));
}
const hasWork = plan.mode === "full" || activePackages.length > 0;
// Gate mode always has work: the merge-gate suite is not covered by the
// per-package cache (it spans engine + cli with its own selection), so it
// must never short-circuit through the cache-fresh fast path.
const hasWork = plan.mode === "full" || plan.mode === "gate" || activePackages.length > 0;
// Cache-fresh fast path: nothing to run. Emit a fast-path mode line, run only
// the (now cheap) isolation guard, and skip skill-sync, artifact-ensure,
@@ -1177,18 +1194,7 @@ export function main(argv = process.argv.slice(2)) {
try {
if (plan.mode === "full") {
if (plan.reason === "missing-comparison-base") {
console.log(`[test-changed] could not resolve merge-base with ${baseBranch}; running full suite.`);
} else if (plan.reason === "diff-failed") {
console.log("[test-changed] failed to read git diff; running full suite.");
} else if (plan.reason === "no-changes") {
console.log("[test-changed] no changes detected against base; running full suite.");
} else if (plan.reason === "shared-infra-changed") {
console.log("[test-changed] shared/root test infrastructure changed; running full suite.");
} else if (plan.reason === "no-affected-package") {
console.log("[test-changed] no affected workspace package resolved; running full suite.");
}
// Explicit opt-in only ("forced": --full / FUSION_TEST_FULL=1).
runMaybeIsolated("pnpm", [`-r`, `--workspace-concurrency=${workspaceConcurrency}`, "test", ...forwardedArgs], {
env: isolatedHomeEnv,
onBeforeAfterCheck: cleanupIsolatedHome,
@@ -1196,6 +1202,34 @@ export function main(argv = process.argv.slice(2)) {
return;
}
if (plan.mode === "gate") {
if (plan.reason === "missing-comparison-base") {
console.log(`[test-changed] could not resolve merge-base with ${baseBranch}; running merge-gate suite.`);
} else if (plan.reason === "diff-failed") {
console.log("[test-changed] failed to read git diff; running merge-gate suite.");
} else if (plan.reason === "no-changes") {
console.log("[test-changed] no changes detected against base; running merge-gate suite.");
} else if (plan.reason === "shared-infra-changed") {
console.log("[test-changed] shared/root test infrastructure changed; running merge-gate suite.");
} else if (plan.reason === "no-affected-package") {
console.log("[test-changed] no affected workspace package resolved; running merge-gate suite.");
}
console.log("[test-changed] need the full sweep instead? run `pnpm test:full` (explicit opt-in).");
runMaybeIsolated("pnpm", ["test:gate"], {
env: isolatedHomeEnv,
onBeforeAfterCheck: cleanupIsolatedHome,
});
return;
}
// Changed mode: merge-gate suite first, then the affected set. The gate is
// cheap (~10s) and keeps `pnpm test` green ⇒ mergeable-signal honest; the
// affected expansion preserves changed-code coverage. Overlap (engine in the
// affected set re-runs the engine-core files) is accepted by design.
console.log("[test-changed] running merge-gate suite (pnpm test:gate) before affected packages.");
run("pnpm", ["test:gate"], { env: isolatedHomeEnv });
const filterArgs = activePackages.flatMap((pkg) => ["--filter", pkg]);
console.log(`[test-changed] running tests for changed packages: ${activePackages.join(", ")}`);
if (cachedPackages.length > 0) {