fix(telegram): avoid leaking thread binding persist cleanup

This commit is contained in:
Vincent Koc
2026-04-13 16:38:58 +01:00
parent 35176f3cb7
commit c441dcd47a
2 changed files with 57 additions and 2 deletions

View File

@@ -5,6 +5,20 @@ import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runti
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
const writeJsonFileAtomicallyMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/json-store", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/json-store")>(
"openclaw/plugin-sdk/json-store",
);
writeJsonFileAtomicallyMock.mockImplementation(actual.writeJsonFileAtomically);
return {
...actual,
writeJsonFileAtomically: writeJsonFileAtomicallyMock,
};
});
import {
__testing,
createTelegramThreadBindingManager,
@@ -16,6 +30,7 @@ describe("telegram thread bindings", () => {
let stateDirOverride: string | undefined;
beforeEach(async () => {
writeJsonFileAtomicallyMock.mockClear();
await __testing.resetTelegramThreadBindingsForTests();
});
@@ -313,4 +328,43 @@ describe("telegram thread bindings", () => {
};
expect(persisted.bindings?.[0]?.idleTimeoutMs).toBe(90_000);
});
it("does not leak unhandled rejections when a persist write fails", async () => {
stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-"));
process.env.OPENCLAW_STATE_DIR = stateDirOverride;
const unhandled: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
unhandled.push(reason);
};
process.on("unhandledRejection", onUnhandledRejection);
try {
const manager = createTelegramThreadBindingManager({
accountId: "persist-failure",
persist: true,
enableSweeper: false,
});
await getSessionBindingService().bind({
targetSessionKey: "agent:main:subagent:child-persist-failure",
targetKind: "subagent",
conversation: {
channel: "telegram",
accountId: "persist-failure",
conversationId: "-100200300:topic:100",
},
});
writeJsonFileAtomicallyMock.mockImplementationOnce(async () => {
throw new Error("persist boom");
});
manager.touchConversation("-100200300:topic:100");
await __testing.resetTelegramThreadBindingsForTests();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(unhandled).toEqual([]);
} finally {
process.off("unhandledRejection", onUnhandledRejection);
}
});
});

View File

@@ -345,11 +345,12 @@ function enqueuePersistBindings(params: {
await persistBindingsToDisk(params);
});
getThreadBindingsState().persistQueueByAccountId.set(params.accountId, next);
void next.finally(() => {
const cleanup = () => {
if (getThreadBindingsState().persistQueueByAccountId.get(params.accountId) === next) {
getThreadBindingsState().persistQueueByAccountId.delete(params.accountId);
}
});
};
next.then(cleanup, cleanup);
return next;
}