fix(channels): prevent lifecycle listener buildup (#109108)

This commit is contained in:
xingzhou
2026-07-16 13:08:12 -07:00
committed by GitHub
parent da8a443a4d
commit 0e1fad711c
4 changed files with 34 additions and 0 deletions
@@ -1,4 +1,5 @@
// Discord tests cover message handler.queue plugin behavior.
import { getEventListeners } from "node:events";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DiscordRetryableInboundError } from "./inbound-dedupe.js";
@@ -440,6 +441,22 @@ describe("createDiscordMessageHandler queue behavior", () => {
expect(setStatus.mock.calls.length).toBe(callsBeforeStop);
});
it("removes lifecycle abort listeners after handler deactivation", () => {
const abortController = new AbortController();
const initialListenerCount = getEventListeners(abortController.signal, "abort").length;
const handler = createDiscordMessageHandler(
createDiscordHandlerParams({ abortSignal: abortController.signal }),
);
expect(getEventListeners(abortController.signal, "abort")).toHaveLength(
initialListenerCount + 2,
);
handler.deactivate();
expect(getEventListeners(abortController.signal, "abort")).toHaveLength(initialListenerCount);
});
it("skips queued runs that have not started yet after deactivation", async () => {
preflightDiscordMessageMock.mockReset();
processDiscordMessageMock.mockReset();
@@ -82,6 +82,7 @@ export function createDiscordMessageRunQueue(
let lifecycleActive = !params.abortSignal?.aborted;
const cleanupSkippedQueuedMessages = () => {
params.abortSignal?.removeEventListener("abort", cleanupSkippedQueuedMessages);
// These callbacks represent jobs accepted into the queue but not started.
// Running jobs remove their callback before processDiscordMessage owns cleanup.
if (!lifecycleActive && skippedCleanup.size === 0) {
+15
View File
@@ -1,4 +1,5 @@
// Run state machine tests cover channel run lifecycle transitions and terminal states.
import { getEventListeners } from "node:events";
import { describe, expect, it, vi } from "vitest";
import { createRunStateMachine } from "./run-state-machine.js";
@@ -38,4 +39,18 @@ describe("createRunStateMachine", () => {
machine.onRunEnd();
expect(setStatus.mock.calls.length).toBe(callsBeforeAbort);
});
it("removes its abort listener on manual deactivation", () => {
const abortController = new AbortController();
const initialListenerCount = getEventListeners(abortController.signal, "abort").length;
const machine = createRunStateMachine({ abortSignal: abortController.signal });
expect(getEventListeners(abortController.signal, "abort")).toHaveLength(
initialListenerCount + 1,
);
machine.deactivate();
expect(getEventListeners(abortController.signal, "abort")).toHaveLength(initialListenerCount);
});
});
+1
View File
@@ -61,6 +61,7 @@ export function createRunStateMachine(params: RunStateMachineParams) {
const deactivate = () => {
lifecycleActive = false;
clearHeartbeat();
params.abortSignal?.removeEventListener("abort", onAbort);
};
const onAbort = () => {