diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index b57dc8541..aa2da2bf8 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -442,7 +442,10 @@ class SlackAdapter(BasePlatformAdapter): e, exc_info=True, ) - return await super().send_image_file(chat_id, image_path, caption, reply_to) + text = f"🖼️ Image: {image_path}" + if caption: + text = f"{caption}\n{text}" + return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata) async def send_image( self, @@ -549,7 +552,10 @@ class SlackAdapter(BasePlatformAdapter): e, exc_info=True, ) - return await super().send_video(chat_id, video_path, caption, reply_to) + text = f"🎬 Video: {video_path}" + if caption: + text = f"{caption}\n{text}" + return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata) async def send_document( self, @@ -587,7 +593,10 @@ class SlackAdapter(BasePlatformAdapter): e, exc_info=True, ) - return await super().send_document(chat_id, file_path, caption, file_name, reply_to) + text = f"📎 File: {file_path}" + if caption: + text = f"{caption}\n{text}" + return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata) async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: """Get information about a Slack channel.""" diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index e300728cb..bb2535ed6 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -847,3 +847,102 @@ class TestReplyBroadcast: await adapter.send("C123", "hi", metadata={"thread_id": "parent_ts"}) kwargs = adapter._app.client.chat_postMessage.call_args.kwargs assert kwargs.get("reply_broadcast") is True + + +# --------------------------------------------------------------------------- +# TestFallbackPreservesThreadContext +# --------------------------------------------------------------------------- + +class TestFallbackPreservesThreadContext: + """Bug fix: file upload fallbacks lost thread context (metadata) when + calling super() without metadata, causing replies to appear outside + the thread.""" + + @pytest.mark.asyncio + async def test_send_image_file_fallback_preserves_thread(self, adapter, tmp_path): + test_file = tmp_path / "photo.jpg" + test_file.write_bytes(b"\xff\xd8\xff\xe0") + + adapter._app.client.files_upload_v2 = AsyncMock( + side_effect=Exception("upload failed") + ) + adapter._app.client.chat_postMessage = AsyncMock( + return_value={"ts": "msg_ts"} + ) + + metadata = {"thread_id": "parent_ts_123"} + await adapter.send_image_file( + chat_id="C123", + image_path=str(test_file), + caption="test image", + metadata=metadata, + ) + + call_kwargs = adapter._app.client.chat_postMessage.call_args.kwargs + assert call_kwargs.get("thread_ts") == "parent_ts_123" + + @pytest.mark.asyncio + async def test_send_video_fallback_preserves_thread(self, adapter, tmp_path): + test_file = tmp_path / "clip.mp4" + test_file.write_bytes(b"\x00\x00\x00\x1c") + + adapter._app.client.files_upload_v2 = AsyncMock( + side_effect=Exception("upload failed") + ) + adapter._app.client.chat_postMessage = AsyncMock( + return_value={"ts": "msg_ts"} + ) + + metadata = {"thread_id": "parent_ts_456"} + await adapter.send_video( + chat_id="C123", + video_path=str(test_file), + metadata=metadata, + ) + + call_kwargs = adapter._app.client.chat_postMessage.call_args.kwargs + assert call_kwargs.get("thread_ts") == "parent_ts_456" + + @pytest.mark.asyncio + async def test_send_document_fallback_preserves_thread(self, adapter, tmp_path): + test_file = tmp_path / "report.pdf" + test_file.write_bytes(b"%PDF-1.4") + + adapter._app.client.files_upload_v2 = AsyncMock( + side_effect=Exception("upload failed") + ) + adapter._app.client.chat_postMessage = AsyncMock( + return_value={"ts": "msg_ts"} + ) + + metadata = {"thread_id": "parent_ts_789"} + await adapter.send_document( + chat_id="C123", + file_path=str(test_file), + caption="report", + metadata=metadata, + ) + + call_kwargs = adapter._app.client.chat_postMessage.call_args.kwargs + assert call_kwargs.get("thread_ts") == "parent_ts_789" + + @pytest.mark.asyncio + async def test_send_image_file_fallback_includes_caption(self, adapter, tmp_path): + test_file = tmp_path / "photo.jpg" + test_file.write_bytes(b"\xff\xd8\xff\xe0") + + adapter._app.client.files_upload_v2 = AsyncMock( + side_effect=Exception("upload failed") + ) + adapter._app.client.chat_postMessage = AsyncMock( + return_value={"ts": "msg_ts"} + ) + + await adapter.send_image_file( + chat_id="C123", + image_path=str(test_file), + caption="important screenshot", + ) + + call_kwargs = adapter._app.client.chat_postMessage.call_args.kwargs + assert "important screenshot" in call_kwargs["text"]