Skip to content

Commit c441dcd

Browse files
committed
fix(telegram): avoid leaking thread binding persist cleanup
1 parent 35176f3 commit c441dcd

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

extensions/telegram/src/thread-bindings.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ import { getSessionBindingService } from "openclaw/plugin-sdk/conversation-runti
55
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
66
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
77
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
8+
9+
const writeJsonFileAtomicallyMock = vi.hoisted(() => vi.fn());
10+
11+
vi.mock("openclaw/plugin-sdk/json-store", async () => {
12+
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/json-store")>(
13+
"openclaw/plugin-sdk/json-store",
14+
);
15+
writeJsonFileAtomicallyMock.mockImplementation(actual.writeJsonFileAtomically);
16+
return {
17+
...actual,
18+
writeJsonFileAtomically: writeJsonFileAtomicallyMock,
19+
};
20+
});
21+
822
import {
923
__testing,
1024
createTelegramThreadBindingManager,
@@ -16,6 +30,7 @@ describe("telegram thread bindings", () => {
1630
let stateDirOverride: string | undefined;
1731

1832
beforeEach(async () => {
33+
writeJsonFileAtomicallyMock.mockClear();
1934
await __testing.resetTelegramThreadBindingsForTests();
2035
});
2136

@@ -313,4 +328,43 @@ describe("telegram thread bindings", () => {
313328
};
314329
expect(persisted.bindings?.[0]?.idleTimeoutMs).toBe(90_000);
315330
});
331+
332+
it("does not leak unhandled rejections when a persist write fails", async () => {
333+
stateDirOverride = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-telegram-bindings-"));
334+
process.env.OPENCLAW_STATE_DIR = stateDirOverride;
335+
const unhandled: unknown[] = [];
336+
const onUnhandledRejection = (reason: unknown) => {
337+
unhandled.push(reason);
338+
};
339+
process.on("unhandledRejection", onUnhandledRejection);
340+
341+
try {
342+
const manager = createTelegramThreadBindingManager({
343+
accountId: "persist-failure",
344+
persist: true,
345+
enableSweeper: false,
346+
});
347+
348+
await getSessionBindingService().bind({
349+
targetSessionKey: "agent:main:subagent:child-persist-failure",
350+
targetKind: "subagent",
351+
conversation: {
352+
channel: "telegram",
353+
accountId: "persist-failure",
354+
conversationId: "-100200300:topic:100",
355+
},
356+
});
357+
358+
writeJsonFileAtomicallyMock.mockImplementationOnce(async () => {
359+
throw new Error("persist boom");
360+
});
361+
manager.touchConversation("-100200300:topic:100");
362+
363+
await __testing.resetTelegramThreadBindingsForTests();
364+
await new Promise((resolve) => setTimeout(resolve, 0));
365+
expect(unhandled).toEqual([]);
366+
} finally {
367+
process.off("unhandledRejection", onUnhandledRejection);
368+
}
369+
});
316370
});

extensions/telegram/src/thread-bindings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,12 @@ function enqueuePersistBindings(params: {
345345
await persistBindingsToDisk(params);
346346
});
347347
getThreadBindingsState().persistQueueByAccountId.set(params.accountId, next);
348-
void next.finally(() => {
348+
const cleanup = () => {
349349
if (getThreadBindingsState().persistQueueByAccountId.get(params.accountId) === next) {
350350
getThreadBindingsState().persistQueueByAccountId.delete(params.accountId);
351351
}
352-
});
352+
};
353+
next.then(cleanup, cleanup);
353354
return next;
354355
}
355356

0 commit comments

Comments
 (0)