diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 2a1ce9fb..dacacb29 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -1835,56 +1835,51 @@ export class SlackAdapter implements Adapter { channel: string, threadTs?: string ): Promise { - const fileIds: string[] = []; - - for (const file of files) { - try { - // Convert data to Buffer using shared utility - const fileBuffer = await toBuffer(file.data, { platform: "slack" }); - if (!fileBuffer) { - continue; - } - - this.logger.debug("Slack API: files.uploadV2", { - filename: file.filename, - size: fileBuffer.length, - mimeType: file.mimeType, - }); - - // biome-ignore lint/suspicious/noExplicitAny: Slack API types don't match actual usage - const uploadArgs: any = { - channel_id: channel, - filename: file.filename, - file: fileBuffer, - }; - if (threadTs) { - uploadArgs.thread_ts = threadTs; + const bufferResults = await Promise.all( + files.map(async (file) => { + try { + const fileBuffer = await toBuffer(file.data, { platform: "slack" }); + if (!fileBuffer) { + return null; + } + return { file: fileBuffer, filename: file.filename }; + } catch (error) { + this.logger.error("Failed to convert file to buffer", { + filename: file.filename, + error, + }); + return null; } + }) + ); + const fileUploads = bufferResults.filter( + (result): result is NonNullable => result !== null + ); + if (fileUploads.length === 0) { + return []; + } + this.logger.debug("Slack API: files.uploadV2 (batch)", { + fileCount: fileUploads.length, + filenames: fileUploads.map((f) => f.filename), + }); - uploadArgs.token = this.getToken(); - const result = (await this.client.files.uploadV2(uploadArgs)) as { - ok: boolean; - files?: Array<{ id?: string }>; - }; - - this.logger.debug("Slack API: files.uploadV2 response", { - ok: result.ok, - }); - - // Extract file IDs from the response - if (result.files && Array.isArray(result.files)) { - for (const uploadedFile of result.files) { - if (uploadedFile.id) { - fileIds.push(uploadedFile.id); - } - } + // biome-ignore lint/suspicious/noExplicitAny: Slack API types don't match actual usage + const uploadArgs: any = { channel_id: channel, file_uploads: fileUploads }; + if (threadTs) { + uploadArgs.thread_ts = threadTs; + } + uploadArgs.token = this.getToken(); + const result = (await this.client.files.uploadV2(uploadArgs)) as { + ok: boolean; + files?: Array<{ files?: Array<{ id?: string }> }>; + }; + this.logger.debug("Slack API: files.uploadV2 response", { ok: result.ok }); + const fileIds: string[] = []; + if (result.files?.[0]?.files) { + for (const uploadedFile of result.files[0].files) { + if (uploadedFile.id) { + fileIds.push(uploadedFile.id); } - } catch (error) { - this.logger.error("Failed to upload file", { - filename: file.filename, - error, - }); - throw error; } } diff --git a/packages/integration-tests/src/slack.test.ts b/packages/integration-tests/src/slack.test.ts index 75a9020e..ac669d80 100644 --- a/packages/integration-tests/src/slack.test.ts +++ b/packages/integration-tests/src/slack.test.ts @@ -716,7 +716,7 @@ describe("Slack Integration", () => { expect(mockClient.files.uploadV2).toHaveBeenCalledWith( expect.objectContaining({ channel_id: TEST_CHANNEL, - filename: "test.txt", + file_uploads: [expect.objectContaining({ filename: "test.txt" })], }) ); expect(mockClient.chat.postMessage).toHaveBeenCalledWith( @@ -751,7 +751,16 @@ describe("Slack Integration", () => { }); await tracker.waitForAll(); - expect(mockClient.files.uploadV2).toHaveBeenCalledTimes(2); + expect(mockClient.files.uploadV2).toHaveBeenCalledTimes(1); + expect(mockClient.files.uploadV2).toHaveBeenCalledWith( + expect.objectContaining({ + channel_id: TEST_CHANNEL, + file_uploads: [ + expect.objectContaining({ filename: "file1.txt" }), + expect.objectContaining({ filename: "file2.txt" }), + ], + }) + ); }); it("should handle files-only messages (no text)", async () => {