From d2ac0e3b6fcd6c03c5dfdeb88fddfc222e3b4792 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 25 Aug 2026 00:21:23 -0700 Subject: [PATCH] fix(slack): preserve actual multipart delivery receipt kinds (#129089) --- extensions/slack/src/send.blocks.test.ts | 13 ++++++++ extensions/slack/src/send.ts | 41 +++++++++++------------- extensions/slack/src/send.upload.test.ts | 24 +++++++++++++- 3 files changed, 55 insertions(+), 23 deletions(-) diff --git a/extensions/slack/src/send.blocks.test.ts b/extensions/slack/src/send.blocks.test.ts index aa429a4a85e3..7aa6b57e7084 100644 --- a/extensions/slack/src/send.blocks.test.ts +++ b/extensions/slack/src/send.blocks.test.ts @@ -429,6 +429,19 @@ describe("sendMessageSlack blocks", () => { expect( delivered.some((result) => result.receipt.parts[0]?.kind === "card" && !result.meta), ).toBe(true); + expect( + aggregateResult.receipt.parts.map(({ platformMessageId, kind, index }) => ({ + platformMessageId, + kind, + index, + })), + ).toEqual( + delivered.map((result, index) => ({ + platformMessageId: result.messageId, + kind: result.receipt.parts[0]?.kind, + index, + })), + ); const questionDelivery = delivered.find((delivery) => delivery.meta); expect(questionDelivery?.messageId).not.toBe(aggregateResult.messageId); expect(JSON.stringify(aggregateResult.meta)).toBe( diff --git a/extensions/slack/src/send.ts b/extensions/slack/src/send.ts index d3b0c5eec58e..a93b38e558f8 100644 --- a/extensions/slack/src/send.ts +++ b/extensions/slack/src/send.ts @@ -339,6 +339,18 @@ function createSlackSendReceipt(params: { }); } +function createSlackSendReceiptFromResults( + results: readonly SlackSendResult[], + threadTs?: string, +): MessageReceipt { + const receipt = createMessageReceiptFromOutboundResults({ results, threadId: threadTs }); + const thread = threadTs ? { threadId: threadTs } : {}; + for (const [index, part] of receipt.parts.entries()) { + Object.assign(part, { index, ...thread }); + } + return Object.assign(receipt, thread); +} + function resolveToken(params: { explicit?: string; accountId: string; @@ -1134,6 +1146,7 @@ async function sendMessageSlackQueuedInner(params: { accountId: account.accountId, token: delivery.credential, }); + const deliveredResults: SlackSendResult[] = []; const reportDelivery = async ( result: SlackSendResult, deliveredBlocks?: (Block | KnownBlock)[], @@ -1154,6 +1167,7 @@ async function sendMessageSlackQueuedInner(params: { }, } : result; + deliveredResults.push(deliveryResult); await opts.onDeliveryResult?.(deliveryResult); return deliveryResult; }; @@ -1213,7 +1227,6 @@ async function sendMessageSlackQueuedInner(params: { orderedBlockDeliveryPlan?.skipOriginalBlocks ? orderedBlockDeliveryPlan : undefined; - const sentMessageIds: string[] = []; let lastMessageId = ""; let deliveredChannelId = channelId; let canonicalDeliveredThreadTs: string | undefined; @@ -1310,7 +1323,6 @@ async function sendMessageSlackQueuedInner(params: { lastMessageId = response.ts; deliveredChannelId = resolvePostedMessageChannelId(response, deliveredChannelId); canonicalDeliveredThreadTs ??= resolvePostedMessageThreadTs(response); - sentMessageIds.push(response.ts); const deliveredThreadTs = resolvePostedMessageThreadTs(response) ?? normalizeSlackThreadTsCandidate(opts.threadTs); const fallbackDelivery = await reportDelivery( @@ -1331,11 +1343,10 @@ async function sendMessageSlackQueuedInner(params: { questionDelivery = fallbackDelivery; } } - const messageId = lastMessageId; const deliveredThreadTs = canonicalDeliveredThreadTs ?? normalizeSlackThreadTsCandidate(opts.threadTs); return { - messageId, + messageId: lastMessageId, channelId: deliveredChannelId, threadTs: deliveredThreadTs, // Core replaces per-card progress with this aggregate; retain the @@ -1348,12 +1359,7 @@ async function sendMessageSlackQueuedInner(params: { }, } : {}), - receipt: createSlackSendReceipt({ - platformMessageIds: sentMessageIds, - channelId: deliveredChannelId, - kind: fallbackMessages.some((message) => message.blocks) ? "card" : "text", - threadTs: deliveredThreadTs, - }), + receipt: createSlackSendReceiptFromResults(deliveredResults, deliveredThreadTs), }; } } @@ -1391,7 +1397,6 @@ async function sendMessageSlackQueuedInner(params: { onPlatformSendDispatch: dispatchOnce, ...(delivery.upload ? { auditContext: delivery.upload.auditContext } : {}), }); - sentMessageIds.push(lastMessageId); await reportDelivery({ messageId: lastMessageId, channelId, @@ -1409,8 +1414,7 @@ async function sendMessageSlackQueuedInner(params: { } for (const [partIndex, chunk] of chunksToPost.entries()) { - const carriesPrimaryMessageOptions = - partIndex === 0 && !opts.mediaUrl && sentMessageIds.length === 0; + const carriesPrimaryMessageOptions = partIndex === 0 && !opts.mediaUrl; const baseMetadata = carriesPrimaryMessageOptions ? opts.metadata : undefined; // Every post carries its index/count so reconciliation proves the complete // logical text send and never mistakes a partial chunk fanout for success. @@ -1442,7 +1446,6 @@ async function sendMessageSlackQueuedInner(params: { lastMessageId = response.ts; deliveredChannelId = resolvePostedMessageChannelId(response, deliveredChannelId); canonicalDeliveredThreadTs ??= resolvePostedMessageThreadTs(response); - sentMessageIds.push(response.ts); await reportDelivery({ messageId: response.ts, channelId: deliveredChannelId, @@ -1458,19 +1461,13 @@ async function sendMessageSlackQueuedInner(params: { }); } - const messageId = lastMessageId; const deliveredThreadTs = canonicalDeliveredThreadTs ?? normalizeSlackThreadTsCandidate(opts.threadTs); return { - messageId, + messageId: lastMessageId, channelId: deliveredChannelId, threadTs: deliveredThreadTs, - receipt: createSlackSendReceipt({ - platformMessageIds: sentMessageIds, - channelId: deliveredChannelId, - kind: opts.mediaUrl ? "media" : "text", - threadTs: deliveredThreadTs, - }), + receipt: createSlackSendReceiptFromResults(deliveredResults, deliveredThreadTs), }; } /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */ diff --git a/extensions/slack/src/send.upload.test.ts b/extensions/slack/src/send.upload.test.ts index 1c6e5b06bfb1..cd25e90124f4 100644 --- a/extensions/slack/src/send.upload.test.ts +++ b/extensions/slack/src/send.upload.test.ts @@ -579,6 +579,7 @@ describe("sendMessageSlack file upload with user IDs", () => { vi.stubEnv("NO_PROXY", "127.0.0.1,localhost"); vi.stubEnv("no_proxy", "127.0.0.1,localhost"); const alternateClient = createUploadTestClient(`${baseUrl}/api/`); + const onDeliveryResult = vi.fn(); mockUploadDestination(alternateClient, `${baseUrl}/upload/v1/capability`); fetchWithSsrFGuard.mockImplementationOnce(async (params) => { const mockedFetch = globalThis.fetch; @@ -590,9 +591,30 @@ describe("sendMessageSlack file upload with user IDs", () => { } }); - await sendUpload(alternateClient, { mediaUrl: "/tmp/alternate-root.png" }); + const result = await sendUpload(alternateClient, { + mediaUrl: "/tmp/alternate-root.png", + message: "a".repeat(8_500), + threadTs: "171.222", + onDeliveryResult, + }); expectCompletedUpload({ client: alternateClient, expected: { channel_id: "C123CHAN" } }); + expect(alternateClient.chat.postMessage).toHaveBeenCalledOnce(); + expect( + onDeliveryResult.mock.calls.map(([delivery]) => delivery.receipt.parts[0]?.kind), + ).toEqual(["media", "text"]); + expect( + result.receipt.parts.map(({ platformMessageId, kind, index, threadId }) => ({ + platformMessageId, + kind, + index, + threadId, + })), + ).toEqual([ + { platformMessageId: "F001", kind: "media", index: 0, threadId: "171.222" }, + { platformMessageId: "171234.567", kind: "text", index: 1, threadId: "171.222" }, + ]); + expect(result.receipt.threadId).toBe("171.222"); expect(cleanupUploadTimeout).toHaveBeenCalledOnce(); expect(uploadTimeoutControllers).toHaveLength(0); },