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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user