from __future__ import annotations import time from unittest.mock import AsyncMock, MagicMock import pytest from yuxi.channels.adapters.mattermost.adapter import MattermostAdapter from yuxi.channels.adapters.mattermost.cache import LRUCache, TTLCache @pytest.fixture def adapter(): config = { "bot_token": "test-token", "server_url": "https://mattermost.example.com", } return MattermostAdapter(config=config) @pytest.fixture def mock_driver(): driver = MagicMock() driver.posts = MagicMock() driver.posts.create_post = MagicMock(return_value={"id": "msg_001"}) driver.posts.update_post = AsyncMock() driver.posts.delete_post = AsyncMock() driver.posts.pin_post = AsyncMock() driver.posts.unpin_post = AsyncMock() driver.posts.get_pinned_posts = AsyncMock(return_value=[]) driver.posts.search_posts = AsyncMock(return_value={"posts": []}) driver.posts.get_thread = AsyncMock(return_value={"posts": []}) driver.reactions = MagicMock() driver.reactions.create_reaction = AsyncMock() driver.reactions.remove_reaction = AsyncMock() driver.reactions.get_reactions = AsyncMock(return_value=[]) driver.users = MagicMock() driver.users.create_user_typing = AsyncMock() return driver @pytest.fixture def connected_adapter(adapter, mock_driver): adapter._status = adapter._status.__class__.CONNECTED adapter._drivers["default"] = mock_driver adapter._bot_user_id = "bot_001" adapter._bot_username = "testbot" adapter._server_url = "https://mattermost.example.com" return adapter class TestMattermostOperations: @pytest.mark.asyncio async def test_pin_message(self, connected_adapter): result = await connected_adapter.pin_message("post_001") assert result is True @pytest.mark.asyncio async def test_unpin_message(self, connected_adapter): result = await connected_adapter.unpin_message("post_001") assert result is True @pytest.mark.asyncio async def test_list_pinned_messages(self, connected_adapter, mock_driver): mock_driver.posts.get_pinned_posts.return_value = [ {"id": "p1", "message": "pinned post"} ] result = await connected_adapter.list_pinned_messages("channel_001") assert len(result) == 1 assert result[0]["id"] == "p1" @pytest.mark.asyncio async def test_get_reactions(self, connected_adapter, mock_driver): mock_driver.reactions.get_reactions.return_value = [ {"emoji_name": "thumbsup", "user_id": "u1"} ] result = await connected_adapter.get_reactions("post_001") assert len(result) == 1 assert result[0]["emoji_name"] == "thumbsup" @pytest.mark.asyncio async def test_search_messages(self, connected_adapter, mock_driver): mock_driver.posts.search_posts.return_value = { "posts": [{"id": "p1", "message": "hello world"}] } result = await connected_adapter.search_messages("team_001", "hello") assert len(result) == 1 @pytest.mark.asyncio async def test_get_thread_replies(self, connected_adapter, mock_driver): mock_driver.posts.get_thread.return_value = { "posts": [{"id": "p1"}, {"id": "p2"}] } result = await connected_adapter.get_thread_replies("post_001") assert len(result) == 2 @pytest.mark.asyncio async def test_pin_message_not_connected(self, adapter): assert await adapter.pin_message("post_001") is False @pytest.mark.asyncio async def test_unpin_message_not_connected(self, adapter): assert await adapter.unpin_message("post_001") is False class TestMattermostSupportsAction: def test_supports_pin_unpin(self, adapter): assert adapter.supports_action("pin") is True assert adapter.supports_action("unpin") is True def test_supports_existing_actions(self, adapter): assert adapter.supports_action("send") is True assert adapter.supports_action("react") is True assert adapter.supports_action("edit") is True assert adapter.supports_action("delete") is True def test_does_not_support_unknown(self, adapter): assert adapter.supports_action("unknown_action") is False class TestCache: def test_ttlcache_get_set(self): cache = TTLCache(ttl_s=300, max_size=100) cache.set("key1", "value1") assert cache.get("key1") == "value1" def test_ttlcache_delete(self): cache = TTLCache(ttl_s=300, max_size=100) cache.set("key1", "value1") assert cache.delete("key1") is True assert cache.get("key1") is None assert cache.delete("nonexistent") is False def test_lrucache_eviction(self): cache = LRUCache(max_size=2, ttl_s=300) cache.set("a", 1) cache.set("b", 2) cache.set("c", 3) assert cache.size() == 2 assert cache.get("a") is None assert cache.get("b") == 2 assert cache.get("c") == 3 class TestMattermostCapabilities: def test_pin_capabilities_enabled(self, adapter): assert adapter.capabilities.pin is True assert adapter.capabilities.unpin is True assert adapter.capabilities.list_pins is True def test_ephemeral_capability_enabled(self, adapter): assert adapter.capabilities.send_ephemeral is True def test_typing_capability_enabled(self, adapter): assert adapter.capabilities.typing is True class TestMattermostWSConfig: def test_default_ws_config(self): adapter = MattermostAdapter(config={}) assert adapter._ws_ping_interval_s == 30.0 assert adapter._ws_pong_timeout_s == 10.0 assert adapter._ws_reconnect_jitter == 0.2 assert adapter._ws_max_reconnect_attempts == 10 assert adapter._ws_reconnect_max_delay_s == 120.0 def test_custom_ws_config(self): adapter = MattermostAdapter( config={ "bot_token": "test-token", "server_url": "https://mattermost.example.com", "ws_ping_interval_s": 15.0, "ws_pong_timeout_s": 5.0, "ws_reconnect_jitter": 0.5, "ws_max_reconnect_attempts": 5, "ws_reconnect_max_delay_s": 60.0, } ) assert adapter._ws_ping_interval_s == 15.0 assert adapter._ws_pong_timeout_s == 5.0 assert adapter._ws_reconnect_jitter == 0.5 assert adapter._ws_max_reconnect_attempts == 5 assert adapter._ws_reconnect_max_delay_s == 60.0 class TestDedupObservability: def test_dedup_counters_initialized(self, adapter): assert adapter._dedup_hit_count == 0 assert adapter._dedup_total_count == 0 def test_dedup_total_increments(self, adapter): adapter._seen_posts["post_1"] = time.monotonic() adapter._dedup_check("post_2") assert adapter._dedup_total_count == 1 def test_dedup_hit_increments(self, adapter): adapter._seen_posts["post_1"] = time.monotonic() adapter._dedup_check("post_1") assert adapter._dedup_hit_count == 1 assert adapter._dedup_total_count == 1 class TestDedupSnapshot: def test_snapshot_includes_dedup_stats(self, adapter): adapter._dedup_total_count = 10 adapter._dedup_hit_count = 3 snapshot = adapter.snapshot() assert snapshot["dedup_hit_count"] == 3 assert snapshot["dedup_total_count"] == 10 assert snapshot["dedup_hit_rate"] == 0.3 def test_snapshot_dedup_zero_division(self, adapter): snapshot = adapter.snapshot() assert snapshot["dedup_hit_rate"] == 0.0 def test_snapshot_includes_ws_config(self, adapter): snapshot = adapter.snapshot() assert "ws_ping_interval_s" in snapshot assert "ws_pong_timeout_s" in snapshot