diff --git a/extensions/discord/src/durable-delivery.test.ts b/extensions/discord/src/durable-delivery.test.ts new file mode 100644 index 00000000000..49f21a33f94 --- /dev/null +++ b/extensions/discord/src/durable-delivery.test.ts @@ -0,0 +1,103 @@ +import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-message"; +import { + createEmptyPluginRegistry, + createTestRegistry, + resetPluginRuntimeStateForTest, + setActivePluginRegistry, +} from "openclaw/plugin-sdk/plugin-test-runtime"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + createDiscordOutboundHoisted, + installDiscordOutboundModuleSpies, + resetDiscordOutboundMocks, +} from "./outbound-adapter.test-harness.js"; + +const hoisted = createDiscordOutboundHoisted(); +await installDiscordOutboundModuleSpies(hoisted); + +let discordPlugin: typeof import("./channel.js").discordPlugin; + +beforeAll(async () => { + ({ discordPlugin } = await import("./channel.js")); +}); + +describe("durable Discord delivery", () => { + beforeEach(() => { + resetDiscordOutboundMocks(hoisted); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "discord", + source: "test", + plugin: discordPlugin, + }, + ]), + ); + }); + + afterEach(() => { + resetPluginRuntimeStateForTest(); + setActivePluginRegistry(createEmptyPluginRegistry()); + }); + + it("fans out planned text chunks and retries a transient failure on a later chunk", async () => { + hoisted.sendMessageDiscordMock + .mockResolvedValueOnce({ + messageId: "msg-chunk-1", + channelId: "ch-1", + }) + .mockRejectedValueOnce(Object.assign(new Error("discord 500"), { status: 500 })) + .mockResolvedValueOnce({ + messageId: "msg-chunk-2", + channelId: "ch-1", + }); + + const result = await sendDurableMessageBatch({ + cfg: { + channels: { + discord: { + token: "test-token", + retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 }, + }, + }, + }, + channel: "discord", + to: "channel:123456", + payloads: [{ text: "first chunk\nsecond chunk" }], + formatting: { + chunkMode: "newline", + maxLinesPerMessage: 1, + textLimit: 2000, + }, + skipQueue: true, + }); + + expect(result.status).toBe("sent"); + if (result.status !== "sent") { + throw new Error("expected durable Discord send to succeed"); + } + expect( + result.results.map((entry) => ({ + channel: entry.channel, + messageId: entry.messageId, + })), + ).toEqual([ + { channel: "discord", messageId: "msg-chunk-1" }, + { channel: "discord", messageId: "msg-chunk-2" }, + ]); + expect(result.receipt.platformMessageIds).toEqual(["msg-chunk-1", "msg-chunk-2"]); + expect(result.payloadOutcomes).toEqual([ + { + index: 0, + status: "sent", + results: result.results, + }, + ]); + expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledTimes(3); + expect(hoisted.sendMessageDiscordMock.mock.calls.map((call) => call[1])).toEqual([ + "first chunk", + "second chunk", + "second chunk", + ]); + }); +}); diff --git a/extensions/telegram/src/message-cache.ts b/extensions/telegram/src/message-cache.ts index 7a69995f73a..403d2443b73 100644 --- a/extensions/telegram/src/message-cache.ts +++ b/extensions/telegram/src/message-cache.ts @@ -568,7 +568,7 @@ export function createTelegramMessageCache(params?: { } const key = telegramMessageCacheKey({ accountId, chatId, messageId }); const cachedNode = upsertCachedMessageNode({ messages, key, node, mode }); - if (node.messageId === currentObservation.node.messageId) { + if (messageId === currentObservation.node.messageId) { recordedEntry = cachedNode; } trimMessages(messages, maxMessages); @@ -697,11 +697,12 @@ function resolveSessionBoundaryNode(params: { if (!params.messageId) { return undefined; } + const { messageId } = params; const candidates = params.cache .recentBefore({ accountId: params.accountId, chatId: params.chatId, - messageId: params.messageId, + messageId, ...(params.threadId !== undefined ? { threadId: params.threadId } : {}), limit: Number.MAX_SAFE_INTEGER, }) @@ -709,7 +710,7 @@ function resolveSessionBoundaryNode(params: { const current = params.cache.get({ accountId: params.accountId, chatId: params.chatId, - messageId: params.messageId, + messageId, }); if (current && isSessionBoundaryCommandNode(current)) { candidates.push(current); diff --git a/src/scripts/test-projects.test.ts b/src/scripts/test-projects.test.ts index b72064a1b22..6170fa6c162 100644 --- a/src/scripts/test-projects.test.ts +++ b/src/scripts/test-projects.test.ts @@ -943,6 +943,7 @@ describe("test-projects args", () => { "extensions/discord/src/channel-actions.contract.test.ts", "extensions/discord/src/channel.message-adapter.test.ts", "extensions/discord/src/channel.test.ts", + "extensions/discord/src/durable-delivery.test.ts", "extensions/discord/src/monitor/message-handler.bot-self-filter.test.ts", "extensions/discord/src/monitor/message-handler.queue.test.ts", "extensions/discord/src/monitor/provider.skill-dedupe.test.ts",