mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:20:43 +00:00
test(telegram): cover debounce topic keys at seam
This commit is contained in:
@@ -7,3 +7,12 @@ export function buildTelegramInboundDebounceKey(params: {
|
||||
const resolvedAccountId = params.accountId?.trim() || "default";
|
||||
return `telegram:${resolvedAccountId}:${params.conversationKey}:${params.senderId}:${params.debounceLane}`;
|
||||
}
|
||||
|
||||
export function buildTelegramInboundDebounceConversationKey(params: {
|
||||
chatId: number | string;
|
||||
threadId?: number | null;
|
||||
}): string {
|
||||
return params.threadId != null
|
||||
? `${params.chatId}:topic:${params.threadId}`
|
||||
: String(params.chatId);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTelegramInboundDebounceKey } from "./bot-handlers.debounce-key.js";
|
||||
import {
|
||||
buildTelegramInboundDebounceConversationKey,
|
||||
buildTelegramInboundDebounceKey,
|
||||
} from "./bot-handlers.debounce-key.js";
|
||||
|
||||
describe("buildTelegramInboundDebounceKey", () => {
|
||||
it("uses the resolved account id instead of literal default when provided", () => {
|
||||
@@ -23,4 +26,31 @@ describe("buildTelegramInboundDebounceKey", () => {
|
||||
}),
|
||||
).toBe("telegram:default:12345:67890:forward");
|
||||
});
|
||||
|
||||
it("keeps direct topic thread ids in the conversation key", () => {
|
||||
const topic100 = buildTelegramInboundDebounceConversationKey({ chatId: 7, threadId: 100 });
|
||||
const topic200 = buildTelegramInboundDebounceConversationKey({ chatId: 7, threadId: 200 });
|
||||
|
||||
expect(topic100).toBe("7:topic:100");
|
||||
expect(topic200).toBe("7:topic:200");
|
||||
expect(
|
||||
buildTelegramInboundDebounceKey({
|
||||
accountId: "default",
|
||||
conversationKey: topic100,
|
||||
senderId: "42",
|
||||
debounceLane: "default",
|
||||
}),
|
||||
).not.toBe(
|
||||
buildTelegramInboundDebounceKey({
|
||||
accountId: "default",
|
||||
conversationKey: topic200,
|
||||
senderId: "42",
|
||||
debounceLane: "default",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the chat id as the conversation key when no thread is present", () => {
|
||||
expect(buildTelegramInboundDebounceConversationKey({ chatId: 7 })).toBe("7");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,10 @@ import {
|
||||
resolveDefaultAgentId,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "./bot-handlers.agent.runtime.js";
|
||||
import { buildTelegramInboundDebounceKey } from "./bot-handlers.debounce-key.js";
|
||||
import {
|
||||
buildTelegramInboundDebounceConversationKey,
|
||||
buildTelegramInboundDebounceKey,
|
||||
} from "./bot-handlers.debounce-key.js";
|
||||
import {
|
||||
hasInboundMedia,
|
||||
hasReplyTargetMedia,
|
||||
@@ -1165,9 +1168,10 @@ export const registerTelegramHandlers = ({
|
||||
]
|
||||
: [];
|
||||
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
||||
const conversationThreadId = resolvedThreadId ?? dmThreadId;
|
||||
const conversationKey =
|
||||
conversationThreadId != null ? `${chatId}:topic:${conversationThreadId}` : String(chatId);
|
||||
const conversationKey = buildTelegramInboundDebounceConversationKey({
|
||||
chatId,
|
||||
threadId: resolvedThreadId ?? dmThreadId,
|
||||
});
|
||||
const debounceLane = resolveTelegramDebounceLane(msg);
|
||||
const debounceKey = senderId
|
||||
? buildTelegramInboundDebounceKey({
|
||||
|
||||
@@ -1676,89 +1676,6 @@ describe("createTelegramBot", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("isolates inbound debounce by DM topic thread id", async () => {
|
||||
const DEBOUNCE_MS = 4321;
|
||||
onSpy.mockClear();
|
||||
replySpy.mockClear();
|
||||
loadConfig.mockReturnValue({
|
||||
agents: {
|
||||
defaults: {
|
||||
envelopeTimezone: "utc",
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
inbound: {
|
||||
debounceMs: DEBOUNCE_MS,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
||||
try {
|
||||
const repliesDelivered = waitForReplyCalls(2);
|
||||
createTelegramBot({ token: "tok" });
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "private" },
|
||||
text: "topic-100",
|
||||
date: 1736380800,
|
||||
message_id: 201,
|
||||
message_thread_id: 100,
|
||||
from: { id: 42, first_name: "Ada" },
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 7, type: "private" },
|
||||
text: "topic-200",
|
||||
date: 1736380801,
|
||||
message_id: 202,
|
||||
message_thread_id: 200,
|
||||
from: { id: 42, first_name: "Ada" },
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
|
||||
const debounceTimerIndexes = setTimeoutSpy.mock.calls
|
||||
.map((call, index) => ({ index, delay: call[1] }))
|
||||
.filter((entry) => entry.delay === DEBOUNCE_MS)
|
||||
.map((entry) => entry.index);
|
||||
expect(debounceTimerIndexes.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
for (const index of debounceTimerIndexes) {
|
||||
clearTimeout(setTimeoutSpy.mock.results[index]?.value as ReturnType<typeof setTimeout>);
|
||||
}
|
||||
for (const index of debounceTimerIndexes) {
|
||||
const flushTimer = setTimeoutSpy.mock.calls[index]?.[0] as (() => unknown) | undefined;
|
||||
await flushTimer?.();
|
||||
}
|
||||
|
||||
await repliesDelivered;
|
||||
const threadIds = replySpy.mock.calls
|
||||
.map(
|
||||
(call: [unknown, ...unknown[]]) =>
|
||||
(call[0] as { MessageThreadId?: number }).MessageThreadId,
|
||||
)
|
||||
.toSorted((a: number | undefined, b: number | undefined) => (a ?? 0) - (b ?? 0));
|
||||
expect(threadIds).toEqual([100, 200]);
|
||||
} finally {
|
||||
setTimeoutSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("handles quote-only replies without reply metadata", async () => {
|
||||
onSpy.mockClear();
|
||||
sendMessageSpy.mockClear();
|
||||
|
||||
Reference in New Issue
Block a user