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:
7
.changeset/grok-process-lifecycle-owner.md
Normal file
7
.changeset/grok-process-lifecycle-owner.md
Normal 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.
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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[]) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user