From bc15f6cca3b798c39e77b86772a6f9d903c8d2d1 Mon Sep 17 00:00:00 2001 From: Himess Date: Tue, 17 Mar 2026 15:40:03 +0300 Subject: [PATCH] fix(mattermost): use MIME types for media attachments Bare strings like "image", "audio", "document" were appended to media_types, but downstream run.py checks mtype.startswith("image/") and mtype.startswith("audio/"), which never matched. This caused all Mattermost file attachments to be silently dropped from vision/STT processing. Use the actual MIME type from file_info instead. --- gateway/platforms/mattermost.py | 6 +- tests/gateway/test_mattermost.py | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/mattermost.py b/gateway/platforms/mattermost.py index ef1d5b838..7ff939f68 100644 --- a/gateway/platforms/mattermost.py +++ b/gateway/platforms/mattermost.py @@ -617,16 +617,16 @@ class MattermostAdapter(BasePlatformAdapter): if mime.startswith("image/"): local_path = cache_image_from_bytes(file_data, ext or ".png") media_urls.append(local_path) - media_types.append("image") + media_types.append(mime) elif mime.startswith("audio/"): from gateway.platforms.base import cache_audio_from_bytes local_path = cache_audio_from_bytes(file_data, ext or ".ogg") media_urls.append(local_path) - media_types.append("audio") + media_types.append(mime) else: local_path = cache_document_from_bytes(file_data, fname) media_urls.append(local_path) - media_types.append("document") + media_types.append(mime) else: logger.warning("Mattermost: failed to download file %s: HTTP %s", fid, resp.status) except Exception as exc: diff --git a/tests/gateway/test_mattermost.py b/tests/gateway/test_mattermost.py index 6b0fbd899..9f604d2c7 100644 --- a/tests/gateway/test_mattermost.py +++ b/tests/gateway/test_mattermost.py @@ -572,3 +572,102 @@ class TestMattermostRequirements: monkeypatch.delenv("MATTERMOST_URL", raising=False) from gateway.platforms.mattermost import check_mattermost_requirements assert check_mattermost_requirements() is False + + +# --------------------------------------------------------------------------- +# Media type propagation (MIME types, not bare strings) +# --------------------------------------------------------------------------- + +class TestMattermostMediaTypes: + """Verify that media_types contains actual MIME types (e.g. 'image/png') + rather than bare category strings ('image'), so downstream + ``mtype.startswith("image/")`` checks in run.py work correctly.""" + + def setup_method(self): + self.adapter = _make_adapter() + self.adapter._bot_user_id = "bot_user_id" + self.adapter.handle_message = AsyncMock() + + def _make_event(self, file_ids): + post_data = { + "id": "post_media", + "user_id": "user_123", + "channel_id": "chan_456", + "message": "file attached", + "file_ids": file_ids, + } + return { + "event": "posted", + "data": { + "post": json.dumps(post_data), + "channel_type": "O", + "sender_name": "@alice", + }, + } + + @pytest.mark.asyncio + async def test_image_media_type_is_full_mime(self): + """An image attachment should produce 'image/png', not 'image'.""" + file_info = {"name": "photo.png", "mime_type": "image/png"} + self.adapter._api_get = AsyncMock(return_value=file_info) + + mock_resp = AsyncMock() + mock_resp.status = 200 + mock_resp.read = AsyncMock(return_value=b"\x89PNG fake") + mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) + mock_resp.__aexit__ = AsyncMock(return_value=False) + self.adapter._session = MagicMock() + self.adapter._session.get = MagicMock(return_value=mock_resp) + + with patch("gateway.platforms.base.cache_image_from_bytes", return_value="/tmp/photo.png"): + await self.adapter._handle_ws_event(self._make_event(["file1"])) + + msg = self.adapter.handle_message.call_args[0][0] + assert msg.media_types == ["image/png"] + assert msg.media_types[0].startswith("image/") + + @pytest.mark.asyncio + async def test_audio_media_type_is_full_mime(self): + """An audio attachment should produce 'audio/ogg', not 'audio'.""" + file_info = {"name": "voice.ogg", "mime_type": "audio/ogg"} + self.adapter._api_get = AsyncMock(return_value=file_info) + + mock_resp = AsyncMock() + mock_resp.status = 200 + mock_resp.read = AsyncMock(return_value=b"OGG fake") + mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) + mock_resp.__aexit__ = AsyncMock(return_value=False) + self.adapter._session = MagicMock() + self.adapter._session.get = MagicMock(return_value=mock_resp) + + with patch("gateway.platforms.base.cache_audio_from_bytes", return_value="/tmp/voice.ogg"), \ + patch("gateway.platforms.base.cache_image_from_bytes"), \ + patch("gateway.platforms.base.cache_document_from_bytes"): + await self.adapter._handle_ws_event(self._make_event(["file2"])) + + msg = self.adapter.handle_message.call_args[0][0] + assert msg.media_types == ["audio/ogg"] + assert msg.media_types[0].startswith("audio/") + + @pytest.mark.asyncio + async def test_document_media_type_is_full_mime(self): + """A document attachment should produce 'application/pdf', not 'document'.""" + file_info = {"name": "report.pdf", "mime_type": "application/pdf"} + self.adapter._api_get = AsyncMock(return_value=file_info) + + mock_resp = AsyncMock() + mock_resp.status = 200 + mock_resp.read = AsyncMock(return_value=b"PDF fake") + mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) + mock_resp.__aexit__ = AsyncMock(return_value=False) + self.adapter._session = MagicMock() + self.adapter._session.get = MagicMock(return_value=mock_resp) + + with patch("gateway.platforms.base.cache_document_from_bytes", return_value="/tmp/report.pdf"), \ + patch("gateway.platforms.base.cache_image_from_bytes"): + await self.adapter._handle_ws_event(self._make_event(["file3"])) + + msg = self.adapter.handle_message.call_args[0][0] + assert msg.media_types == ["application/pdf"] + assert not msg.media_types[0].startswith("image/") + assert not msg.media_types[0].startswith("audio/")