From d1b76cb1b2cae3f254a021b4a3828a4a1aa16355 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Jan 2026 23:20:52 +0100 Subject: [PATCH] test: cover replyToMode behavior --- src/discord/monitor.test.ts | 47 +++++++++++++++++++++++++ src/discord/monitor.ts | 39 ++++++++++++++------- src/telegram/bot.test.ts | 68 +++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 12 deletions(-) diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts index 220fb14a7..de1d24d4a 100644 --- a/src/discord/monitor.test.ts +++ b/src/discord/monitor.test.ts @@ -7,6 +7,7 @@ import { resolveDiscordChannelConfig, resolveDiscordGuildEntry, resolveGroupDmAllow, + resolveDiscordReplyTarget, } from "./monitor.js"; const fakeGuild = (id: string, name: string) => @@ -160,3 +161,49 @@ describe("discord group DM gating", () => { ).toBe(false); }); }); + +describe("discord reply target selection", () => { + it("skips replies when mode is off", () => { + expect( + resolveDiscordReplyTarget({ + replyToMode: "off", + replyToId: "123", + hasReplied: false, + }), + ).toBeUndefined(); + }); + + it("replies only once when mode is first", () => { + expect( + resolveDiscordReplyTarget({ + replyToMode: "first", + replyToId: "123", + hasReplied: false, + }), + ).toBe("123"); + expect( + resolveDiscordReplyTarget({ + replyToMode: "first", + replyToId: "123", + hasReplied: true, + }), + ).toBeUndefined(); + }); + + it("replies on every message when mode is all", () => { + expect( + resolveDiscordReplyTarget({ + replyToMode: "all", + replyToId: "123", + hasReplied: false, + }), + ).toBe("123"); + expect( + resolveDiscordReplyTarget({ + replyToMode: "all", + replyToId: "123", + hasReplied: true, + }), + ).toBe("123"); + }); +}); diff --git a/src/discord/monitor.ts b/src/discord/monitor.ts index 817d5a4ca..cf9d611e8 100644 --- a/src/discord/monitor.ts +++ b/src/discord/monitor.ts @@ -67,6 +67,18 @@ export type DiscordChannelConfigResolved = { requireMention?: boolean; }; +export function resolveDiscordReplyTarget(opts: { + replyToMode: ReplyToMode; + replyToId?: string; + hasReplied: boolean; +}): string | undefined { + if (opts.replyToMode === "off") return undefined; + const replyToId = opts.replyToId?.trim(); + if (!replyToId) return undefined; + if (opts.replyToMode === "all") return replyToId; + return opts.hasReplied ? undefined : replyToId; +} + function summarizeAllowList(list?: Array) { if (!list || list.length === 0) return "any"; const sample = list.slice(0, 4).map((entry) => String(entry)); @@ -1000,19 +1012,20 @@ async function deliverReplies({ const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); const text = payload.text ?? ""; - const replyToId = - replyToMode === "off" ? undefined : payload.replyToId?.trim(); + const replyToId = payload.replyToId; if (!text && mediaList.length === 0) continue; if (mediaList.length === 0) { for (const chunk of chunkText(text, 2000)) { + const replyTo = resolveDiscordReplyTarget({ + replyToMode, + replyToId, + hasReplied, + }); await sendMessageDiscord(target, chunk, { token, - replyTo: - replyToId && (replyToMode === "all" || !hasReplied) - ? replyToId - : undefined, + replyTo, }); - if (replyToId && !hasReplied) { + if (replyTo && !hasReplied) { hasReplied = true; } } @@ -1021,15 +1034,17 @@ async function deliverReplies({ for (const mediaUrl of mediaList) { const caption = first ? text : ""; first = false; + const replyTo = resolveDiscordReplyTarget({ + replyToMode, + replyToId, + hasReplied, + }); await sendMessageDiscord(target, caption, { token, mediaUrl, - replyTo: - replyToId && (replyToMode === "all" || !hasReplied) - ? replyToId - : undefined, + replyTo, }); - if (replyToId && !hasReplied) { + if (replyTo && !hasReplied) { hasReplied = true; } } diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 383fd9386..e27273507 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -189,6 +189,74 @@ describe("createTelegramBot", () => { } }); + it("honors replyToMode=first for threaded replies", async () => { + onSpy.mockReset(); + sendMessageSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType< + typeof vi.fn + >; + replySpy.mockReset(); + replySpy.mockResolvedValue({ + text: "a".repeat(4500), + replyToId: "101", + }); + + createTelegramBot({ token: "tok", replyToMode: "first" }); + const handler = onSpy.mock.calls[0][1] as ( + ctx: Record, + ) => Promise; + await handler({ + message: { + chat: { id: 5, type: "private" }, + text: "hi", + date: 1736380800, + message_id: 101, + }, + me: { username: "clawdis_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); + const [first, ...rest] = sendMessageSpy.mock.calls; + expect(first?.[2]?.reply_to_message_id).toBe(101); + for (const call of rest) { + expect(call[2]?.reply_to_message_id).toBeUndefined(); + } + }); + + it("honors replyToMode=all for threaded replies", async () => { + onSpy.mockReset(); + sendMessageSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType< + typeof vi.fn + >; + replySpy.mockReset(); + replySpy.mockResolvedValue({ + text: "a".repeat(4500), + replyToId: "101", + }); + + createTelegramBot({ token: "tok", replyToMode: "all" }); + const handler = onSpy.mock.calls[0][1] as ( + ctx: Record, + ) => Promise; + await handler({ + message: { + chat: { id: 5, type: "private" }, + text: "hi", + date: 1736380800, + message_id: 101, + }, + me: { username: "clawdis_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(sendMessageSpy.mock.calls.length).toBeGreaterThan(1); + for (const call of sendMessageSpy.mock.calls) { + expect(call[2]?.reply_to_message_id).toBe(101); + } + }); + it("skips group messages without mention when requireMention is enabled", async () => { onSpy.mockReset(); const replySpy = replyModule.__replySpy as unknown as ReturnType<