Files
fusion/plugins/fusion-plugin-omp-runtime/src/__tests__/process-lifecycle.test.ts
gsxdsm 2ab0413c07 fix: make OMP process lifecycle tests full-suite safe (#2290)
## Summary
After #2289, Full Suite shard 4 still failed on the **OMP** twin of the
Grok process-lifecycle stress test (`import("../index.js")` × 15 under
shard transform load → 5s timeout).

Apply the same fix class as grok-runtime:
- Symbol.for exit reaper on `process-manager`
- Stress test reimports that module
- 15s timeout for cold transform

## Test plan
- [x] Local OMP process-lifecycle green
- [ ] PR gate
- [ ] Post-merge Full Suite

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

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved cleanup of OMP ACP processes when the application exits.
- Prevented duplicate exit handlers and excess listener growth during
runtime reloads.
- Preserved reliable process lifecycle behavior under repeated module
loading.

- **Tests**
  - Added lifecycle coverage for repeated process-manager reloads.
- Optimized the stress test to complete more efficiently while retaining
cleanup assertions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-18 01:19:14 -07:00

61 lines
2.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
const EVENTS = ["exit", "beforeExit", "SIGTERM", "SIGINT"] as const;
function listenerCounts(): Record<(typeof EVENTS)[number], number> {
return Object.fromEntries(EVENTS.map((event) => [event, process.listenerCount(event)])) as Record<
(typeof EVENTS)[number],
number
>;
}
describe("OMP plugin process lifecycle", () => {
afterEach(() => {
vi.resetModules();
});
/*
FNXC:OmpRuntimeTests 2026-07-18-08:10:
Same full-suite class as grok-runtime: prove Symbol.for exit-hook bound by
re-importing process-manager (lifecycle owner), not the full plugin graph,
with a 15s cold-transform budget under shard load.
*/
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 < 5; iteration += 1) {
vi.resetModules();
await import("../acp/process-manager.js");
}
await new Promise<void>((resolve) => setImmediate(resolve));
} finally {
process.off("warning", onWarning);
}
const after = listenerCounts();
expect(after.exit - baseline.exit).toBeLessThanOrEqual(1);
expect(after.beforeExit - baseline.beforeExit).toBe(0);
expect(after.SIGTERM - baseline.SIGTERM).toBe(0);
expect(after.SIGINT - baseline.SIGINT).toBe(0);
expect(warnings.filter((warning) => warning.name === "MaxListenersExceededWarning")).toEqual([]);
const manager = await import("../acp/process-manager.js");
const child = {
killed: false,
exitCode: null,
kill: vi.fn(),
on: vi.fn(),
};
manager.registerProcess(child as never);
for (const cleanup of process.listeners("exit")) {
if (cleanup.name === "killAllProcesses") cleanup(0);
}
expect(child.kill).toHaveBeenCalledWith("SIGKILL");
expect(manager.activeProcessCount()).toBe(0);
});
});