From b3b49bed802eae6df542569615a4a37e6ce09489 Mon Sep 17 00:00:00 2001 From: Marcus Castro Date: Fri, 13 Feb 2026 14:09:04 -0300 Subject: [PATCH] fix(slack): override video/* MIME to audio/* for voice messages (#14941) * fix(slack): override video/* MIME to audio/* for voice messages * fix(slack): preserve overridden MIME in return value * test(slack): fix media monitor MIME mock wiring --------- Co-authored-by: Peter Steinberger --- src/slack/monitor/media.test.ts | 74 +++++++++++++++++++++++++++++++++ src/slack/monitor/media.ts | 22 +++++++++- src/slack/types.ts | 1 + 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/slack/monitor/media.test.ts b/src/slack/monitor/media.test.ts index d9b35ab74b..dd1b3b41ac 100644 --- a/src/slack/monitor/media.test.ts +++ b/src/slack/monitor/media.test.ts @@ -238,6 +238,80 @@ describe("resolveSlackMedia", () => { expect(mockFetch).not.toHaveBeenCalled(); }); + it("overrides video/* MIME to audio/* for slack_audio voice messages", async () => { + // saveMediaBuffer re-detects MIME from buffer bytes, so it may return + // video/mp4 for MP4 containers. Verify resolveSlackMedia preserves + // the overridden audio/* type in its return value despite this. + const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({ + path: "/tmp/voice.mp4", + contentType: "video/mp4", + }); + + const mockResponse = new Response(Buffer.from("audio data"), { + status: 200, + headers: { "content-type": "video/mp4" }, + }); + mockFetch.mockResolvedValueOnce(mockResponse); + + const result = await resolveSlackMedia({ + files: [ + { + url_private: "https://files.slack.com/voice.mp4", + name: "audio_message.mp4", + mimetype: "video/mp4", + subtype: "slack_audio", + }, + ], + token: "xoxb-test-token", + maxBytes: 16 * 1024 * 1024, + }); + + expect(result).not.toBeNull(); + // saveMediaBuffer should receive the overridden audio/mp4 + expect(saveMediaBufferMock).toHaveBeenCalledWith( + expect.any(Buffer), + "audio/mp4", + "inbound", + 16 * 1024 * 1024, + ); + // Returned contentType must be the overridden value, not the + // re-detected video/mp4 from saveMediaBuffer + expect(result!.contentType).toBe("audio/mp4"); + }); + + it("preserves original MIME for non-voice Slack files", async () => { + const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({ + path: "/tmp/video.mp4", + contentType: "video/mp4", + }); + + const mockResponse = new Response(Buffer.from("video data"), { + status: 200, + headers: { "content-type": "video/mp4" }, + }); + mockFetch.mockResolvedValueOnce(mockResponse); + + const result = await resolveSlackMedia({ + files: [ + { + url_private: "https://files.slack.com/clip.mp4", + name: "recording.mp4", + mimetype: "video/mp4", + }, + ], + token: "xoxb-test-token", + maxBytes: 16 * 1024 * 1024, + }); + + expect(result).not.toBeNull(); + expect(saveMediaBufferMock).toHaveBeenCalledWith( + expect.any(Buffer), + "video/mp4", + "inbound", + 16 * 1024 * 1024, + ); + }); + it("falls through to next file when first file returns error", async () => { vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({ path: "/tmp/test.jpg", diff --git a/src/slack/monitor/media.ts b/src/slack/monitor/media.ts index c96ca50234..e634a30dcb 100644 --- a/src/slack/monitor/media.ts +++ b/src/slack/monitor/media.ts @@ -115,6 +115,23 @@ export async function fetchWithSlackAuth(url: string, token: string): Promise params.maxBytes) { continue; } + const effectiveMime = resolveSlackMediaMimetype(file, fetched.contentType); const saved = await saveMediaBuffer( fetched.buffer, - fetched.contentType ?? file.mimetype, + effectiveMime, "inbound", params.maxBytes, ); const label = fetched.fileName ?? file.name; return { path: saved.path, - contentType: saved.contentType, + contentType: effectiveMime ?? saved.contentType, placeholder: label ? `[Slack file: ${label}]` : "[Slack file]", }; } catch { diff --git a/src/slack/types.ts b/src/slack/types.ts index b87bdd739f..39a8d04ae1 100644 --- a/src/slack/types.ts +++ b/src/slack/types.ts @@ -2,6 +2,7 @@ export type SlackFile = { id?: string; name?: string; mimetype?: string; + subtype?: string; size?: number; url_private?: string; url_private_download?: string;