from __future__ import annotations import pytest from yuxi.channels.adapters.mattermost.adapter import MattermostAdapter from yuxi.channels.models import ( ChannelIdentity, ChannelMessage, ChannelResponse, ChannelStatus, ChannelType, ChatType, EventType, HealthStatus, MessageType, ) def make_ws_data(event="posted", post=None, channel_data=None, broadcast=None): data = {} if post: data["post"] = '{"id":"m1","channel_id":"ch1","user_id":"u1","message":"hello"}' if channel_data: data["channel"] = '{"type":"D","display_name":"tester"}' return { "event": event, "data": data, "broadcast": broadcast or {}, } @pytest.fixture def adapter(): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } return MattermostAdapter(config=config) class TestMattermostAdapterMeta: def test_channel_id(self, adapter): assert adapter.channel_id == "mattermost" def test_channel_type(self, adapter): assert adapter.channel_type == ChannelType.MATTERMOST def test_capabilities(self, adapter): assert adapter.supports_streaming is True assert adapter.supports_markdown is True assert adapter.text_chunk_limit == 16383 assert adapter.max_media_size_mb == 100 assert adapter.streaming_modes == ["off", "partial", "block", "progress"] def test_initial_status(self, adapter): assert adapter._status == ChannelStatus.DISCONNECTED def test_webhook_path_is_none(self, adapter): assert adapter.webhook_path is None class TestNormalizeInbound: def test_normalize_text_message(self, adapter): adapter._bot_username = "yuxi" raw = make_ws_data(post=True, channel_data=True) raw["data"]["channel"] = '{"type":"O","display_name":"general"}' result = adapter.normalize_inbound(raw) assert isinstance(result, ChannelMessage) assert result.content == "hello" assert result.identity.channel_id == "mattermost" assert result.identity.channel_type == ChannelType.MATTERMOST assert result.identity.channel_user_id == "u1" assert result.identity.channel_chat_id == "channel_ch1" assert result.identity.channel_message_id == "m1" assert result.event_type == EventType.MESSAGE_RECEIVED assert result.message_type == MessageType.TEXT assert result.chat_type == ChatType.GROUP def test_normalize_with_bot_mention(self, adapter): adapter._bot_username = "yuxi" raw = make_ws_data( post=True, channel_data=True, ) import json raw["data"]["post"] = json.dumps({"id": "m1", "channel_id": "ch1", "user_id": "u1", "message": "hey @yuxi help"}) result = adapter.normalize_inbound(raw) assert result.mentions is not None assert result.mentions.is_bot_mentioned is True assert "yuxi" in result.mentions.mentioned_user_ids def test_normalize_post_edited(self, adapter): adapter._bot_username = "yuxi" raw = make_ws_data(event="post_edited", post=True, channel_data=True) result = adapter.normalize_inbound(raw) assert result.event_type == EventType.MESSAGE_UPDATED def test_normalize_command(self, adapter): import json adapter._bot_username = "yuxi" raw = make_ws_data(post=True, channel_data=True) raw["data"]["post"] = json.dumps({"id": "m1", "channel_id": "ch1", "user_id": "u1", "message": "/reset"}) result = adapter.normalize_inbound(raw) assert result.message_type == MessageType.COMMAND def test_normalize_group_channel(self, adapter): import json adapter._bot_username = "yuxi" raw = make_ws_data(post=True, channel_data=True) raw["data"]["channel"] = '{"type":"O","display_name":"general"}' result = adapter.normalize_inbound(raw) assert result.chat_type == ChatType.GROUP def test_normalize_thread_message(self, adapter): import json adapter._bot_username = "yuxi" raw = make_ws_data(post=True, channel_data=True) raw["data"]["post"] = json.dumps({"id": "m1", "channel_id": "ch1", "user_id": "u1", "message": "reply", "root_id": "root99"}) result = adapter.normalize_inbound(raw) assert result.chat_type == ChatType.THREAD assert result.reply_to_message_id == "root99" def test_normalize_metadata(self, adapter): import json adapter._bot_username = "yuxi" raw = make_ws_data( post=True, channel_data=True, broadcast={"channel_id": "ch1", "team_id": "team_xxx"}, ) raw["data"]["channel"] = '{"type":"D","display_name":"tester_dm"}' result = adapter.normalize_inbound(raw) assert result.metadata["team_id"] == "team_xxx" assert result.metadata["channel_name"] == "tester_dm" class TestFormatOutbound: def test_format_text_message(self, adapter): identity = ChannelIdentity( channel_id="mattermost", channel_type=ChannelType.MATTERMOST, channel_user_id="u1", channel_chat_id="ch1", ) response = ChannelResponse(identity=identity, content="hello world") payload = adapter.format_outbound(response) assert payload["channel_id"] == "ch1" assert payload["message"] == "hello world" assert payload["root_id"] == "" def test_format_with_reply(self, adapter): identity = ChannelIdentity( channel_id="mattermost", channel_type=ChannelType.MATTERMOST, channel_user_id="u1", channel_chat_id="ch1", ) response = ChannelResponse(identity=identity, content="reply", reply_to_message_id="root1") payload = adapter.format_outbound(response) assert payload["root_id"] == "root1" def test_format_with_attachments(self, adapter): identity = ChannelIdentity( channel_id="mattermost", channel_type=ChannelType.MATTERMOST, channel_user_id="u1", channel_chat_id="ch1", ) response = ChannelResponse( identity=identity, content="check this", metadata={"file_ids": ["f1", "f2"]}, ) payload = adapter.format_outbound(response) assert payload["file_ids"] == ["f1", "f2"] class TestHealthCheck: def test_health_check_disconnected(self, adapter): import asyncio async def _check(): result = await adapter.health_check() assert result.status == "unhealthy" asyncio.run(_check()) class TestPreConnect: @pytest.mark.asyncio async def test_pre_connect_missing_config(self): adapter = MattermostAdapter(config={}) result = await adapter.pre_connect() assert result["status"] == "error" assert "Missing" in result["message"]