From e4ddfe0218cc73adb3e851d78d664da3676550a0 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 18 Jul 2026 00:54:33 -0700 Subject: [PATCH] fix: make Grok process lifecycle tests full-suite safe (#2289) ## Summary Full Suite after #2288 still red on shard 4: `process-lifecycle.test.ts` times out at 5s under shard transform load even after reducing reimports. - Move Symbol.for `process.exit` reaper onto `process-manager` (lifecycle owner) - Stress test reimports that module (not the full plugin graph) - Explicit 15s timeout for the cold-transform bound stress test ## Test plan - [x] Local process-lifecycle green (~2s) - [ ] PR merge gate - [ ] Post-merge Full Suite green ## Summary by CodeRabbit - **Bug Fixes** - Improved Grok runtime process cleanup during application shutdown. - Prevented duplicate cleanup handlers from accumulating during repeated module loading. - Ensured managed processes are reliably terminated when the process exits. - **Tests** - Expanded lifecycle coverage to validate repeated loading scenarios. - Increased test timeouts for more reliable stress-test execution. --- .changeset/grok-process-lifecycle-owner.md | 7 ++++++ .../src/__tests__/process-lifecycle.test.ts | 16 ++++++------- .../src/acp/process-manager.ts | 15 ++++++++++++ .../fusion-plugin-grok-runtime/src/index.ts | 23 ++++++------------- 4 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 .changeset/grok-process-lifecycle-owner.md diff --git a/.changeset/grok-process-lifecycle-owner.md b/.changeset/grok-process-lifecycle-owner.md new file mode 100644 index 0000000000..39c004f594 --- /dev/null +++ b/.changeset/grok-process-lifecycle-owner.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep Grok ACP process cleanup armed once per process, without listener growth. +category: fix +dev: Move Symbol.for process.exit reaper onto process-manager; lifecycle tests reimport that module instead of the full plugin graph under full-suite load. diff --git a/plugins/fusion-plugin-grok-runtime/src/__tests__/process-lifecycle.test.ts b/plugins/fusion-plugin-grok-runtime/src/__tests__/process-lifecycle.test.ts index 4ac15f874a..c1c1917b23 100644 --- a/plugins/fusion-plugin-grok-runtime/src/__tests__/process-lifecycle.test.ts +++ b/plugins/fusion-plugin-grok-runtime/src/__tests__/process-lifecycle.test.ts @@ -15,22 +15,22 @@ describe("Grok plugin process lifecycle", () => { }); /* - FNXC:GrokRuntimeTests 2026-07-18-07:25: - Symbol.for exit-hook guard is proven by two re-evaluations after the baseline - import. Full-suite shard transform load made 5–15 dynamic imports of the full - plugin graph hit the default 5s testTimeout; keep the bound assertion, not the - iteration marathon. + FNXC:GrokRuntimeTests 2026-07-18-07:40: + Prove the process-manager Symbol.for exit-hook guard by re-importing the + registry module (lifecycle owner), not the full plugin graph. Full-suite + shard transform of @fusion/core via process-manager can still exceed the + default 5s budget on cold workers — give the bound stress test 15s. */ - it("keeps its process cleanup owner bounded across repeated module evaluation", async () => { + it("keeps its process cleanup owner bounded across repeated module evaluation", { timeout: 15_000 }, async () => { const baseline = listenerCounts(); const warnings: Error[] = []; const onWarning = (warning: Error) => warnings.push(warning); process.on("warning", onWarning); try { - for (let iteration = 0; iteration < 2; iteration += 1) { + for (let iteration = 0; iteration < 5; iteration += 1) { vi.resetModules(); - await import("../index.js"); + await import("../acp/process-manager.js"); } await new Promise((resolve) => setImmediate(resolve)); } finally { diff --git a/plugins/fusion-plugin-grok-runtime/src/acp/process-manager.ts b/plugins/fusion-plugin-grok-runtime/src/acp/process-manager.ts index fd6922c8d6..bb491101c1 100644 --- a/plugins/fusion-plugin-grok-runtime/src/acp/process-manager.ts +++ b/plugins/fusion-plugin-grok-runtime/src/acp/process-manager.ts @@ -84,6 +84,21 @@ export function killAllProcesses(): void { activeProcesses.clear(); } +/* +FNXC:ProcessLifecycle 2026-07-18-07:40: +Install the process.exit reaper here (not only from index.ts) so lifecycle +ownership lives with the registry module. Full-suite shards previously timed +out process-lifecycle.test while repeatedly importing the full plugin graph +just to exercise Symbol.for; re-importing this module is enough to prove the +bound and still runs when the plugin entry loads killAllProcesses. +*/ +const PROCESS_EXIT_HOOK_KEY = Symbol.for("fusion.plugin.grok-runtime.exitCleanup"); +const processWithExitHook = process as typeof process & { [key: symbol]: boolean | undefined }; +if (!processWithExitHook[PROCESS_EXIT_HOOK_KEY]) { + process.on("exit", killAllProcesses); + processWithExitHook[PROCESS_EXIT_HOOK_KEY] = true; +} + export class MissingAcpEnvError extends Error { readonly code = "ACP_MISSING_ENV"; constructor(readonly missingKeys: string[]) { diff --git a/plugins/fusion-plugin-grok-runtime/src/index.ts b/plugins/fusion-plugin-grok-runtime/src/index.ts index 2e26ecda6f..30bcb6cba2 100644 --- a/plugins/fusion-plugin-grok-runtime/src/index.ts +++ b/plugins/fusion-plugin-grok-runtime/src/index.ts @@ -1,6 +1,12 @@ import { definePlugin } from "@fusion/plugin-sdk"; import type { FusionPlugin } from "@fusion/plugin-sdk"; -import { killAllProcesses } from "./acp/index.js"; +/* +FNXC:ProcessLifecycle 2026-07-16-07:00 / 2026-07-18-07:40: +Exit-hook ownership lives in ./acp/process-manager (Symbol.for guard + shared +registry). Import that module from the plugin entry so plugin load still arms +the process.exit reaper; re-evaluation stays bounded by the Symbol.for guard. +*/ +import "./acp/process-manager.js"; import { probeGrokBinary } from "./probe.js"; import { discoverGrokProviderModels } from "./provider.js"; import { GrokRuntimeAdapter } from "./runtime-adapter.js"; @@ -25,21 +31,6 @@ fusion-plugin-acp-runtime, so bundled Grok does not depend on the experimental ACP example plugin package. */ -/* -FNXC:ProcessLifecycle 2026-07-16-07:00: -The dashboard backfill worker repeatedly evaluates this plugin through -`vi.resetModules()` while retaining the process singleton. Install one exit -listener per Grok lifecycle owner and use the process-shared registry in the -ACP manager so it reaps children from every evaluation. Do not appease this -with `setMaxListeners`; the listener must stay bounded. -*/ -const PROCESS_EXIT_HOOK_KEY = Symbol.for("fusion.plugin.grok-runtime.exitCleanup"); -const processWithExitHook = process as typeof process & { [key: symbol]: boolean | undefined }; -if (!processWithExitHook[PROCESS_EXIT_HOOK_KEY]) { - process.on("exit", killAllProcesses); - processWithExitHook[PROCESS_EXIT_HOOK_KEY] = true; -} - const plugin: FusionPlugin = definePlugin({ manifest: { id: "fusion-plugin-grok-runtime",