from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channels.base import BaseChannelAdapter from yuxi.channels.models import ( ChannelIdentity, ChannelMessage, ChannelResponse, ChannelType, DeliveryResult, EventType, HealthStatus, ) class ConcreteAdapter(BaseChannelAdapter): channel_id = "test" channel_type = ChannelType.WEBCHAT async def connect(self) -> None: pass async def disconnect(self) -> None: pass async def send(self, response: ChannelResponse) -> DeliveryResult: return DeliveryResult(success=True) def normalize_inbound(self, raw) -> ChannelMessage: raise NotImplementedError def format_outbound(self, response: ChannelResponse): return {"content": response.content} async def health_check(self) -> HealthStatus: return HealthStatus(status="healthy") class TestAbstractMethods: def test_cannot_instantiate_without_abstract_methods(self): with pytest.raises(TypeError): class IncompleteAdapter(BaseChannelAdapter): channel_id = "incomplete" channel_type = ChannelType.WEBCHAT IncompleteAdapter() def test_can_instantiate_concrete_adapter(self): adapter = ConcreteAdapter() assert adapter.channel_id == "test" @pytest.mark.asyncio async def test_abstract_connect(self): adapter = ConcreteAdapter() await adapter.connect() @pytest.mark.asyncio async def test_abstract_send_returns_delivery_result(self): adapter = ConcreteAdapter() identity = ChannelIdentity( channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1" ) response = ChannelResponse(identity=identity, content="hello") result = await adapter.send(response) assert isinstance(result, DeliveryResult) assert result.success is True class TestDefaultImplementations: def setup_method(self): self.adapter = ConcreteAdapter() @pytest.mark.asyncio async def test_send_media_raises_not_implemented(self): with pytest.raises(NotImplementedError): await self.adapter.send_media("c1", "image", b"data") @pytest.mark.asyncio async def test_edit_message_raises_not_implemented(self): with pytest.raises(NotImplementedError): await self.adapter.edit_message("c1", "msg1", "new content") @pytest.mark.asyncio async def test_delete_message_raises_not_implemented(self): with pytest.raises(NotImplementedError): await self.adapter.delete_message("c1", "msg1") @pytest.mark.asyncio async def test_send_reaction_raises_not_implemented(self): with pytest.raises(NotImplementedError): await self.adapter.send_reaction("c1", "msg1", "👍") @pytest.mark.asyncio async def test_get_user_info_raises_not_implemented(self): with pytest.raises(NotImplementedError): await self.adapter.get_user_info("u1") @pytest.mark.asyncio async def test_download_media_raises_not_implemented(self): with pytest.raises(NotImplementedError): await self.adapter.download_media("file1") @pytest.mark.asyncio async def test_verify_webhook_returns_true(self): result = await self.adapter.verify_webhook_signature({}, b"body") assert result is True @pytest.mark.asyncio async def test_refresh_token_returns_true(self): result = await self.adapter._refresh_token_if_needed() assert result is True @pytest.mark.asyncio async def test_pre_connect_returns_empty_dict(self): result = await self.adapter.pre_connect() assert result == {} @pytest.mark.asyncio async def test_send_stream_chunk(self): identity = ChannelIdentity( channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="", channel_chat_id="c1", channel_message_id="msg1", ) response = ChannelResponse(identity=identity, content="chunk") adapter = ConcreteAdapter() adapter.send = AsyncMock(return_value=DeliveryResult(success=True)) result = await adapter.send_stream_chunk("c1", "msg1", "chunk", False) assert result.success is True adapter.send.assert_called_once() @pytest.mark.asyncio async def test_receive_is_empty_generator(self): adapter = ConcreteAdapter() items = [] async for msg in adapter.receive(): items.append(msg) assert items == [] class TestClassVars: def test_default_text_chunk_limit(self): assert ConcreteAdapter.text_chunk_limit == 4096 def test_default_supports_markdown(self): assert ConcreteAdapter.supports_markdown is False def test_default_streaming_modes(self): assert ConcreteAdapter.streaming_modes == ["off"] def test_default_max_media_size_mb(self): assert ConcreteAdapter.max_media_size_mb == 100 def test_default_webhook_path(self): assert ConcreteAdapter.webhook_path is None def test_supports_streaming_default(self): assert ConcreteAdapter.supports_streaming is False class TestCallback: def setup_method(self): self.adapter = ConcreteAdapter() @pytest.mark.asyncio async def test_on_message_registers_handler(self): handler = AsyncMock() self.adapter.on_message(handler) assert self.adapter._message_handler is handler @pytest.mark.asyncio async def test_handle_message_calls_handler(self): handler = AsyncMock() self.adapter.on_message(handler) identity = ChannelIdentity( channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1" ) message = ChannelMessage(identity=identity, content="hello") await self.adapter._handle_message(message) handler.assert_called_once_with(message) @pytest.mark.asyncio async def test_handle_message_noop_when_no_handler(self): identity = ChannelIdentity( channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1" ) message = ChannelMessage(identity=identity, content="hello") await self.adapter._handle_message(message) class TestInit: def test_init_with_config(self): adapter = ConcreteAdapter(config={"enabled": True, "display_name": "Test"}) assert adapter.config["enabled"] is True def test_init_without_config(self): adapter = ConcreteAdapter() assert adapter.config == {} assert adapter._message_handler is None