Skip to content

Commit 6a8a655

Browse files
authored
test(discord): cover durable chunk retry delivery (#82898)
* test(discord): cover durable chunk retry delivery * test(discord): use plugin sdk test runtime * fix(telegram): satisfy message cache strict checks * test(discord): include durable delivery in changed lane
1 parent df23b0f commit 6a8a655

3 files changed

Lines changed: 108 additions & 3 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { sendDurableMessageBatch } from "openclaw/plugin-sdk/channel-message";
2+
import {
3+
createEmptyPluginRegistry,
4+
createTestRegistry,
5+
resetPluginRuntimeStateForTest,
6+
setActivePluginRegistry,
7+
} from "openclaw/plugin-sdk/plugin-test-runtime";
8+
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
9+
import {
10+
createDiscordOutboundHoisted,
11+
installDiscordOutboundModuleSpies,
12+
resetDiscordOutboundMocks,
13+
} from "./outbound-adapter.test-harness.js";
14+
15+
const hoisted = createDiscordOutboundHoisted();
16+
await installDiscordOutboundModuleSpies(hoisted);
17+
18+
let discordPlugin: typeof import("./channel.js").discordPlugin;
19+
20+
beforeAll(async () => {
21+
({ discordPlugin } = await import("./channel.js"));
22+
});
23+
24+
describe("durable Discord delivery", () => {
25+
beforeEach(() => {
26+
resetDiscordOutboundMocks(hoisted);
27+
setActivePluginRegistry(
28+
createTestRegistry([
29+
{
30+
pluginId: "discord",
31+
source: "test",
32+
plugin: discordPlugin,
33+
},
34+
]),
35+
);
36+
});
37+
38+
afterEach(() => {
39+
resetPluginRuntimeStateForTest();
40+
setActivePluginRegistry(createEmptyPluginRegistry());
41+
});
42+
43+
it("fans out planned text chunks and retries a transient failure on a later chunk", async () => {
44+
hoisted.sendMessageDiscordMock
45+
.mockResolvedValueOnce({
46+
messageId: "msg-chunk-1",
47+
channelId: "ch-1",
48+
})
49+
.mockRejectedValueOnce(Object.assign(new Error("discord 500"), { status: 500 }))
50+
.mockResolvedValueOnce({
51+
messageId: "msg-chunk-2",
52+
channelId: "ch-1",
53+
});
54+
55+
const result = await sendDurableMessageBatch({
56+
cfg: {
57+
channels: {
58+
discord: {
59+
token: "test-token",
60+
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
61+
},
62+
},
63+
},
64+
channel: "discord",
65+
to: "channel:123456",
66+
payloads: [{ text: "first chunk\nsecond chunk" }],
67+
formatting: {
68+
chunkMode: "newline",
69+
maxLinesPerMessage: 1,
70+
textLimit: 2000,
71+
},
72+
skipQueue: true,
73+
});
74+
75+
expect(result.status).toBe("sent");
76+
if (result.status !== "sent") {
77+
throw new Error("expected durable Discord send to succeed");
78+
}
79+
expect(
80+
result.results.map((entry) => ({
81+
channel: entry.channel,
82+
messageId: entry.messageId,
83+
})),
84+
).toEqual([
85+
{ channel: "discord", messageId: "msg-chunk-1" },
86+
{ channel: "discord", messageId: "msg-chunk-2" },
87+
]);
88+
expect(result.receipt.platformMessageIds).toEqual(["msg-chunk-1", "msg-chunk-2"]);
89+
expect(result.payloadOutcomes).toEqual([
90+
{
91+
index: 0,
92+
status: "sent",
93+
results: result.results,
94+
},
95+
]);
96+
expect(hoisted.sendMessageDiscordMock).toHaveBeenCalledTimes(3);
97+
expect(hoisted.sendMessageDiscordMock.mock.calls.map((call) => call[1])).toEqual([
98+
"first chunk",
99+
"second chunk",
100+
"second chunk",
101+
]);
102+
});
103+
});

extensions/telegram/src/message-cache.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ export function createTelegramMessageCache(params?: {
568568
}
569569
const key = telegramMessageCacheKey({ accountId, chatId, messageId });
570570
const cachedNode = upsertCachedMessageNode({ messages, key, node, mode });
571-
if (node.messageId === currentObservation.node.messageId) {
571+
if (messageId === currentObservation.node.messageId) {
572572
recordedEntry = cachedNode;
573573
}
574574
trimMessages(messages, maxMessages);
@@ -697,19 +697,20 @@ function resolveSessionBoundaryNode(params: {
697697
if (!params.messageId) {
698698
return undefined;
699699
}
700+
const { messageId } = params;
700701
const candidates = params.cache
701702
.recentBefore({
702703
accountId: params.accountId,
703704
chatId: params.chatId,
704-
messageId: params.messageId,
705+
messageId,
705706
...(params.threadId !== undefined ? { threadId: params.threadId } : {}),
706707
limit: Number.MAX_SAFE_INTEGER,
707708
})
708709
.filter(isSessionBoundaryCommandNode);
709710
const current = params.cache.get({
710711
accountId: params.accountId,
711712
chatId: params.chatId,
712-
messageId: params.messageId,
713+
messageId,
713714
});
714715
if (current && isSessionBoundaryCommandNode(current)) {
715716
candidates.push(current);

src/scripts/test-projects.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ describe("test-projects args", () => {
943943
"extensions/discord/src/channel-actions.contract.test.ts",
944944
"extensions/discord/src/channel.message-adapter.test.ts",
945945
"extensions/discord/src/channel.test.ts",
946+
"extensions/discord/src/durable-delivery.test.ts",
946947
"extensions/discord/src/monitor/message-handler.bot-self-filter.test.ts",
947948
"extensions/discord/src/monitor/message-handler.queue.test.ts",
948949
"extensions/discord/src/monitor/provider.skill-dedupe.test.ts",

0 commit comments

Comments
 (0)