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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

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

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-18 00:54:33 -07:00
committed by GitHub
parent 5c7ed8b26f
commit e4ddfe0218
4 changed files with 37 additions and 24 deletions

View File

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

View File

@@ -15,22 +15,22 @@ describe("Grok plugin process lifecycle", () => {
}); });
/* /*
FNXC:GrokRuntimeTests 2026-07-18-07:25: FNXC:GrokRuntimeTests 2026-07-18-07:40:
Symbol.for exit-hook guard is proven by two re-evaluations after the baseline Prove the process-manager Symbol.for exit-hook guard by re-importing the
import. Full-suite shard transform load made 5–15 dynamic imports of the full registry module (lifecycle owner), not the full plugin graph. Full-suite
plugin graph hit the default 5s testTimeout; keep the bound assertion, not the shard transform of @fusion/core via process-manager can still exceed the
iteration marathon. 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 baseline = listenerCounts();
const warnings: Error[] = []; const warnings: Error[] = [];
const onWarning = (warning: Error) => warnings.push(warning); const onWarning = (warning: Error) => warnings.push(warning);
process.on("warning", onWarning); process.on("warning", onWarning);
try { try {
for (let iteration = 0; iteration < 2; iteration += 1) { for (let iteration = 0; iteration < 5; iteration += 1) {
vi.resetModules(); vi.resetModules();
await import("../index.js"); await import("../acp/process-manager.js");
} }
await new Promise<void>((resolve) => setImmediate(resolve)); await new Promise<void>((resolve) => setImmediate(resolve));
} finally { } finally {

View File

@@ -84,6 +84,21 @@ export function killAllProcesses(): void {
activeProcesses.clear(); 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 { export class MissingAcpEnvError extends Error {
readonly code = "ACP_MISSING_ENV"; readonly code = "ACP_MISSING_ENV";
constructor(readonly missingKeys: string[]) { constructor(readonly missingKeys: string[]) {

View File

@@ -1,6 +1,12 @@
import { definePlugin } from "@fusion/plugin-sdk"; import { definePlugin } from "@fusion/plugin-sdk";
import type { FusionPlugin } 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 { probeGrokBinary } from "./probe.js";
import { discoverGrokProviderModels } from "./provider.js"; import { discoverGrokProviderModels } from "./provider.js";
import { GrokRuntimeAdapter } from "./runtime-adapter.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. 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({ const plugin: FusionPlugin = definePlugin({
manifest: { manifest: {
id: "fusion-plugin-grok-runtime", id: "fusion-plugin-grok-runtime",