From e25ae55879360e58f22ba9757c6274d5b0f9d47a Mon Sep 17 00:00:00 2001 From: CHISEN Kaoru Date: Sat, 7 Feb 2026 08:10:58 +0000 Subject: [PATCH] fix(discord): replyToMode first behaviour --- src/auto-reply/reply/formatting.test.ts | 23 ++++++++++++++++++++++- src/auto-reply/reply/reply-reference.ts | 12 +++++------- src/discord/monitor/threading.test.ts | 15 +++++++++++++++ src/slack/monitor/replies.ts | 5 ++++- 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/auto-reply/reply/formatting.test.ts b/src/auto-reply/reply/formatting.test.ts index 38729bf9a49..e6fb0689881 100644 --- a/src/auto-reply/reply/formatting.test.ts +++ b/src/auto-reply/reply/formatting.test.ts @@ -200,14 +200,35 @@ describe("createReplyReferencePlanner", () => { expect(planner.use()).toBe("parent"); }); - it("prefers existing thread id regardless of mode", () => { + it("respects replyToMode off even with existingId", () => { const planner = createReplyReferencePlanner({ replyToMode: "off", existingId: "thread-1", startId: "parent", }); + expect(planner.use()).toBeUndefined(); + expect(planner.hasReplied()).toBe(false); + }); + + it("uses existingId once when mode is first", () => { + const planner = createReplyReferencePlanner({ + replyToMode: "first", + existingId: "thread-1", + startId: "parent", + }); expect(planner.use()).toBe("thread-1"); expect(planner.hasReplied()).toBe(true); + expect(planner.use()).toBeUndefined(); + }); + + it("uses existingId on every call when mode is all", () => { + const planner = createReplyReferencePlanner({ + replyToMode: "all", + existingId: "thread-1", + startId: "parent", + }); + expect(planner.use()).toBe("thread-1"); + expect(planner.use()).toBe("thread-1"); }); it("honors allowReference=false", () => { diff --git a/src/auto-reply/reply/reply-reference.ts b/src/auto-reply/reply/reply-reference.ts index 5a3427e832e..903f011f9f6 100644 --- a/src/auto-reply/reply/reply-reference.ts +++ b/src/auto-reply/reply/reply-reference.ts @@ -32,20 +32,18 @@ export function createReplyReferencePlanner(options: { if (options.replyToMode === "off") { return undefined; } - if (existingId) { - hasReplied = true; - return existingId; - } - if (!startId) { + const id = existingId ?? startId; + if (!id) { return undefined; } if (options.replyToMode === "all") { hasReplied = true; - return startId; + return id; } + // "first": only the first reply gets a reference. if (!hasReplied) { hasReplied = true; - return startId; + return id; } return undefined; }; diff --git a/src/discord/monitor/threading.test.ts b/src/discord/monitor/threading.test.ts index e0a5c537d8a..2b59bc45362 100644 --- a/src/discord/monitor/threading.test.ts +++ b/src/discord/monitor/threading.test.ts @@ -93,7 +93,22 @@ describe("resolveDiscordReplyDeliveryPlan", () => { threadChannel: { id: "thread" }, createdThreadId: null, }); + // "all" returns the reference on every call. expect(plan.replyReference.use()).toBe("m1"); + expect(plan.replyReference.use()).toBe("m1"); + }); + + it("uses existingId only on first call with replyToMode first inside a thread", () => { + const plan = resolveDiscordReplyDeliveryPlan({ + replyTarget: "channel:thread", + replyToMode: "first", + messageId: "m1", + threadChannel: { id: "thread" }, + createdThreadId: null, + }); + // "first" returns the reference only once. + expect(plan.replyReference.use()).toBe("m1"); + expect(plan.replyReference.use()).toBeUndefined(); }); }); diff --git a/src/slack/monitor/replies.ts b/src/slack/monitor/replies.ts index c759ca0b500..550bb9c66b2 100644 --- a/src/slack/monitor/replies.ts +++ b/src/slack/monitor/replies.ts @@ -89,8 +89,11 @@ function createSlackReplyReferencePlanner(params: { messageTs: string | undefined; hasReplied?: boolean; }) { + // When already inside a Slack thread, always stay in it regardless of + // replyToMode — thread_ts is required to keep messages in the thread. + const effectiveMode = params.incomingThreadTs ? "all" : params.replyToMode; return createReplyReferencePlanner({ - replyToMode: params.replyToMode, + replyToMode: effectiveMode, existingId: params.incomingThreadTs, startId: params.messageTs, hasReplied: params.hasReplied,