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:
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<void>((resolve) => setImmediate(resolve));
} finally {

View File

@@ -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[]) {

View File

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