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 -->
This commit is contained in:
7
.changeset/omp-process-lifecycle-owner.md
Normal file
7
.changeset/omp-process-lifecycle-owner.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep OMP ACP process cleanup armed once per process, without listener growth.
|
||||
category: fix
|
||||
dev: Mirror grok-runtime — Symbol.for process.exit reaper on process-manager; lifecycle stress test reimports that module under full-suite load.
|
||||
@@ -2711,8 +2711,11 @@ describe("QuickEntryBox", () => {
|
||||
it("resets Fast toggle to standard after successful task creation", async () => {
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("quick-entry-fast-toggle")).toBeTruthy();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("quick-entry-fast-toggle"));
|
||||
fireEvent.change(textarea, { target: { value: "First fast task" } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
@@ -2726,9 +2729,15 @@ describe("QuickEntryBox", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
/*
|
||||
FNXC:DashboardTests 2026-07-18-08:15:
|
||||
Wait for create to leave the "Creating..." disabled state before re-expanding;
|
||||
full-suite observed onCreate while isSubmitting still true and the fast toggle unmounted.
|
||||
*/
|
||||
await waitForSubmitSuccessToClear(textarea);
|
||||
|
||||
expandQuickEntry();
|
||||
const fastToggle = screen.getByTestId("quick-entry-fast-toggle");
|
||||
const fastToggle = await screen.findByTestId("quick-entry-fast-toggle");
|
||||
expect(fastToggle.getAttribute("aria-pressed")).toBe("false");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Second standard task" } });
|
||||
|
||||
@@ -317,6 +317,13 @@ export default defineConfig({
|
||||
"src/__tests__/self-healing-meta-archive-guards.test.ts",
|
||||
"src/__tests__/triage-token-usage.test.ts",
|
||||
/*
|
||||
FNXC:EngineTests 2026-07-18-08:15:
|
||||
heartbeat-error-recovery timed out at 30s on full-suite shard 1
|
||||
(run 29636550951) for the rotation-shaped 401 recovery case under
|
||||
load without product-bug evidence — quarantine on sight per AGENTS.md.
|
||||
*/
|
||||
"src/__tests__/heartbeat-error-recovery.test.ts",
|
||||
/*
|
||||
FNXC:EngineTests 2026-06-14-02:11:
|
||||
FN-6433 rescued the AI-merge suites by replacing broad activeSessionRegistry cleanup with path-scoped cleanup, so the default engine lane should execute them again. The soft-delete blocker residue suite was deleted under the ratchet because deterministic soft-delete deadlock coverage already owns that invariant.
|
||||
*/
|
||||
|
||||
@@ -14,16 +14,22 @@ describe("OMP plugin process lifecycle", () => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("keeps its process cleanup owner bounded across repeated module evaluation", async () => {
|
||||
/*
|
||||
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 < 15; 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 {
|
||||
|
||||
@@ -84,6 +84,19 @@ export function killAllProcesses(): void {
|
||||
activeProcesses.clear();
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:ProcessLifecycle 2026-07-18-08:10:
|
||||
Install the process.exit reaper here (not only from index.ts) so lifecycle
|
||||
ownership lives with the registry module — same fix class as grok-runtime
|
||||
after full-suite shard timeouts on repeated full-plugin imports.
|
||||
*/
|
||||
const PROCESS_EXIT_HOOK_KEY = Symbol.for("fusion.plugin.omp-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[]) {
|
||||
|
||||
@@ -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-08:10:
|
||||
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 { probeOmpBinary } from "./probe.js";
|
||||
import { discoverOmpProviderModels } from "./provider.js";
|
||||
import { OmpRuntimeAdapter } from "./runtime-adapter.js";
|
||||
@@ -17,21 +23,6 @@ download or bundle it. Upstream: https://omp.sh/docs/acp
|
||||
https://github.com/can1357/oh-my-pi
|
||||
*/
|
||||
|
||||
/*
|
||||
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 OMP 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.omp-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-omp-runtime",
|
||||
|
||||
@@ -85,6 +85,11 @@
|
||||
"file": "packages/engine/src/__tests__/reliability-interactions/meta-chain-auto-close.test.ts",
|
||||
"reason": "VAL-REMOVAL-005 PG migration: reliability fixture uses PG-backed store but sync APIs (getRunAuditEvents, getDatabase) fail in backend mode. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29344576232. Mirrored in packages/engine/vitest.config.ts.",
|
||||
"quarantinedAt": "2026-07-14"
|
||||
},
|
||||
{
|
||||
"file": "packages/engine/src/__tests__/heartbeat-error-recovery.test.ts",
|
||||
"reason": "Full-suite shard 1 timeout (30s) on rotation-shaped 401 recovery under load without product bug evidence. Failing run: https://github.com/Runfusion/Fusion/actions/runs/29636550951. Mirrored in packages/engine/vitest.config.ts.",
|
||||
"quarantinedAt": "2026-07-18"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user