from __future__ import annotations from unittest.mock import patch import pytest from yuxi.channel.outbound.downgrade import Downgrader from yuxi.channel.plugins.protocol import DeliveryCapabilities, OutboundMessage @pytest.fixture def text_message(): return OutboundMessage(content="hello", content_type="text") @pytest.fixture def markdown_message(): return OutboundMessage(content="**bold** and `code`", content_type="markdown") @pytest.fixture def interactive_message(): return OutboundMessage( content={ "title": "Choose", "description": "Pick one", "options": [{"label": "A", "value": "a"}, {"label": "B", "value": "b"}], "buttons": [{"label": "OK", "action_type": "postback", "value": "ok"}], }, content_type="interactive", ) @pytest.fixture def media_message(): return OutboundMessage(content="see this", content_type="text", media=[{"url": "http://x/a.jpg"}]) async def test_text_unchanged_when_supported(text_message): caps = DeliveryCapabilities( max_text_length=100, supports_markdown=False, supports_interactive=False, supports_media=False, ) result = await Downgrader.downgrade(text_message, caps) assert result.content == "hello" assert result.content_type == "text" async def test_markdown_downgraded_to_text(markdown_message): caps = DeliveryCapabilities(supports_markdown=False, max_text_length=1000) result = await Downgrader.downgrade(markdown_message, caps) assert result.content_type == "text" assert "**bold**" not in result.content assert result.content == "bold and code" async def test_markdown_kept_when_supported(markdown_message): caps = DeliveryCapabilities(supports_markdown=True, max_text_length=1000) result = await Downgrader.downgrade(markdown_message, caps) assert result.content_type == "markdown" assert result.content == "**bold** and `code`" async def test_interactive_downgraded_to_markdown(interactive_message): caps = DeliveryCapabilities(supports_interactive=False, supports_markdown=True, max_text_length=4000) result = await Downgrader.downgrade(interactive_message, caps) assert result.content_type == "markdown" assert "Choose" in result.content assert "A" in result.content assert "OK" in result.content async def test_interactive_downgraded_to_text_when_markdown_unsupported(interactive_message): caps = DeliveryCapabilities(supports_interactive=False, supports_markdown=False, max_text_length=4000) result = await Downgrader.downgrade(interactive_message, caps) assert result.content_type == "text" assert "Choose" in result.content async def test_media_dropped_when_unsupported(media_message): caps = DeliveryCapabilities(supports_media=False, max_text_length=4000) result = await Downgrader.downgrade(media_message, caps) assert result.media == [] assert "媒体附件" in result.content async def test_media_kept_when_supported(media_message): caps = DeliveryCapabilities(supports_media=True, max_text_length=4000) result = await Downgrader.downgrade(media_message, caps) assert result.media == media_message.media assert "媒体附件" not in result.content async def test_media_drop_without_existing_content(): msg = OutboundMessage(content="", content_type="text", media=[{"file": "a.jpg"}]) caps = DeliveryCapabilities(supports_media=False, max_text_length=4000) result = await Downgrader.downgrade(msg, caps) assert result.content.startswith("[该消息包含") async def test_markdown_to_text_async_uses_thread_for_long_text(): long_text = "**bold**" + "x" * Downgrader._SYNC_REGEX_THRESHOLD msg = OutboundMessage(content=long_text, content_type="markdown") caps = DeliveryCapabilities(supports_markdown=False, max_text_length=100000) with patch("asyncio.to_thread") as mock_to_thread: mock_to_thread.return_value = "sync-result" result = await Downgrader.downgrade(msg, caps) mock_to_thread.assert_awaited_once() assert result.content == "sync-result" async def test_markdown_to_text_sync_for_short_text(): msg = OutboundMessage(content="**bold**", content_type="markdown") caps = DeliveryCapabilities(supports_markdown=False, max_text_length=1000) with patch("asyncio.to_thread") as mock_to_thread: result = await Downgrader.downgrade(msg, caps) mock_to_thread.assert_not_called() assert result.content == "bold" def test_interactive_to_markdown_string_passthrough(): result = Downgrader._interactive_to_markdown("plain") assert result == "plain" def test_interactive_to_markdown_list(): result = Downgrader._interactive_to_markdown([{"title": "A"}, {"title": "B"}]) assert "A" in result assert "B" in result def test_interactive_to_markdown_non_serializable(): class Unserializable: def __str__(self): return "unserializable-value" result = Downgrader._interactive_to_markdown(Unserializable()) assert result == "unserializable-value"