test: cover replyToMode behavior

This commit is contained in:
Peter Steinberger
2026-01-02 23:20:52 +01:00
parent 2c92ccd66e
commit d1b76cb1b2
3 changed files with 142 additions and 12 deletions

View File

@@ -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");
});
});

View File

@@ -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<string | number>) {
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;
}
}

View File

@@ -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<string, unknown>,
) => Promise<void>;
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<string, unknown>,
) => Promise<void>;
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<