From 49b233caa1cdac805a5899717ef12d7134739560 Mon Sep 17 00:00:00 2001 From: Bek <66288351+bek91@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:40:47 -0400 Subject: [PATCH] fix(slack): preserve thread aliases in runtime outbound sends (#62947) Slack-threaded direct sends that go through the generic runtime wrapper now stay in the intended thread when the caller supplies threadTs. --- .../channel-outbound-send.test.ts | 57 +++++++++++++++++++ src/cli/send-runtime/channel-outbound-send.ts | 3 +- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/cli/send-runtime/channel-outbound-send.test.ts b/src/cli/send-runtime/channel-outbound-send.test.ts index f37ae9502a4..712fabb3046 100644 --- a/src/cli/send-runtime/channel-outbound-send.test.ts +++ b/src/cli/send-runtime/channel-outbound-send.test.ts @@ -115,6 +115,63 @@ describe("createChannelOutboundRuntimeSend", () => { ); }); + it("forwards Slack threadTs alias to threadId", async () => { + const sendText = vi.fn(async () => ({ channel: "slack", messageId: "slack-1" })); + mocks.loadChannelOutboundAdapter.mockResolvedValue({ + sendText, + }); + + const { createChannelOutboundRuntimeSend } = await import("./channel-outbound-send.js"); + const runtimeSend = createChannelOutboundRuntimeSend({ + channelId: "slack" as never, + unavailableMessage: "unavailable", + }); + + await runtimeSend.sendMessage("C123", "hello", { + cfg: {}, + threadTs: "1712345678.123456", + }); + + expect(sendText).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: {}, + to: "C123", + text: "hello", + threadId: "1712345678.123456", + }), + ); + }); + + it("prefers canonical thread fields over Slack aliases", async () => { + const sendText = vi.fn(async () => ({ channel: "slack", messageId: "slack-2" })); + mocks.loadChannelOutboundAdapter.mockResolvedValue({ + sendText, + }); + + const { createChannelOutboundRuntimeSend } = await import("./channel-outbound-send.js"); + const runtimeSend = createChannelOutboundRuntimeSend({ + channelId: "slack" as never, + unavailableMessage: "unavailable", + }); + + await runtimeSend.sendMessage("C123", "hello", { + cfg: {}, + messageThreadId: "200.000", + threadId: "150.000", + threadTs: "100.000", + replyToMessageId: "400.000", + replyToId: "300.000", + }); + + expect(sendText).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: {}, + threadId: "200.000", + replyToId: "400.000", + }), + ); + }); + it("falls back to sendText when media is present but sendMedia is unavailable", async () => { const sendText = vi.fn(async () => ({ channel: "whatsapp", messageId: "wa-3" })); mocks.loadChannelOutboundAdapter.mockResolvedValue({ diff --git a/src/cli/send-runtime/channel-outbound-send.ts b/src/cli/send-runtime/channel-outbound-send.ts index 57bbd4e9597..bd51071e7dd 100644 --- a/src/cli/send-runtime/channel-outbound-send.ts +++ b/src/cli/send-runtime/channel-outbound-send.ts @@ -14,6 +14,7 @@ type RuntimeSendOpts = { accountId?: string; threadId?: string | number | null; messageThreadId?: string | number; + threadTs?: string | number; replyToId?: string | number | null; replyToMessageId?: string | number; silent?: boolean; @@ -23,7 +24,7 @@ type RuntimeSendOpts = { }; function resolveRuntimeThreadId(opts: RuntimeSendOpts): string | number | undefined { - return opts.messageThreadId ?? opts.threadId ?? undefined; + return opts.messageThreadId ?? opts.threadId ?? opts.threadTs ?? undefined; } function resolveRuntimeReplyToId(opts: RuntimeSendOpts): string | undefined {