## 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 -->
61 lines
2.1 KiB
TypeScript
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);
|
|
});
|
|
});
|