from __future__ import annotations import asyncio import time import pytest from yuxi.channels.adapters.imessage.echo_cache import ( EchoCache, TEXT_TTL_S, MESSAGE_ID_TTL_S, SELF_CHAT_TTL_S, MAX_SELF_CHAT_ENTRIES, ) from yuxi.channels.adapters.imessage.rate_limiter import LoopRateLimiter from yuxi.channels.adapters.imessage.reflection_guard import ReflectionGuard class TestEchoCache: @pytest.fixture def cache(self): return EchoCache() def test_initial_empty(self, cache): assert cache.is_echo(message_id="any") is False assert cache.is_echo(text="any") is False def test_record_and_detect_by_message_id(self, cache): cache.record_sent("msg_001", "Hello", "chat_001") assert cache.is_echo(message_id="msg_001") is True def test_record_and_detect_by_text(self, cache): cache.record_sent("msg_001", "Hello world", "chat_001") assert cache.is_echo(text="Hello world", chat_guid="chat_001") is True def test_no_false_positive_text(self, cache): cache.record_sent("msg_001", "Hello", "chat_001") assert cache.is_echo(text="Different", chat_guid="chat_001") is False def test_no_false_positive_different_chat(self, cache): cache.record_sent("msg_001", "Hello", "chat_001") assert cache.is_echo(text="Hello", chat_guid="chat_002") is False def test_record_and_detect_by_text_backed_by_id(self, cache): cache.record_sent("msg_001", "Hello", "chat_001") cache._by_message_id.clear() assert cache.is_echo(message_id="msg_001") is True def test_text_ttl_expiry(self, cache): cache._text_ttl = 0.01 cache.record_sent("msg_001", "Hello", "chat_001") time.sleep(0.02) assert cache.is_echo(text="Hello", chat_guid="chat_001") is False def test_message_id_ttl_expiry(self, cache): cache._text_ttl = 0.01 cache._text_backed_by_id_ttl = 0.01 cache._msg_id_ttl = 0.01 cache.record_sent("msg_001", "Hello", "chat_001") time.sleep(0.02) assert cache.is_echo(message_id="msg_001") is False def test_is_self_chat_echo_same_handle(self, cache): result = cache.is_self_chat_echo( "+8613800138000", "+8613800138000", "chat_001" ) assert result is True def test_is_self_chat_echo_different_handle(self, cache): result = cache.is_self_chat_echo( "+8613800138000", "+8613900139000", "chat_001" ) assert result is False def test_is_self_chat_echo_no_self_handle(self, cache): result = cache.is_self_chat_echo( "+8613800138000", "", "chat_001" ) assert result is False def test_is_self_chat_echo_clean_handles(self, cache): result = cache.is_self_chat_echo( " +8613800138000 ", " +8613800138000 ", "chat_001" ) assert result is True def test_record_self_chat_text(self, cache): cache.record_self_chat_text("Hello from self", "chat_001") assert cache.is_in_self_chat_cache("Hello from self", "chat_001") is True def test_record_self_chat_text_different_text(self, cache): cache.record_self_chat_text("Hello", "chat_001") assert cache.is_in_self_chat_cache("Different", "chat_001") is False def test_self_chat_ttl_expiry(self, cache): cache._self_chat_ttl = 0.01 cache.record_self_chat_text("Hello", "chat_001") time.sleep(0.02) assert cache.is_in_self_chat_cache("Hello", "chat_001") is False def test_max_self_chat_entries(self, cache): cache._max_self_chat_entries = 5 for i in range(10): cache.record_self_chat_text(f"msg_{i}", "chat_001") assert len(cache._self_chat_cache) <= 5 def test_cleanup_removes_expired_text_backed_by_id(self, cache): cache._text_backed_by_id_ttl = 0.01 cache.record_sent("msg_001", "Hello", "chat_001") time.sleep(0.02) cache._cleanup() assert "msg_001" not in cache._by_text_backed_by_id def test_default_ttls(self): cache = EchoCache() assert cache._text_ttl == TEXT_TTL_S assert cache._msg_id_ttl == MESSAGE_ID_TTL_S assert cache._self_chat_ttl == SELF_CHAT_TTL_S assert cache._max_self_chat_entries == MAX_SELF_CHAT_ENTRIES def test_multiple_messages_same_text(self, cache): cache.record_sent("msg_001", "Hello", "chat_001") cache.record_sent("msg_002", "Hello", "chat_001") assert cache.is_echo(message_id="msg_001") is True assert cache.is_echo(message_id="msg_002") is True assert cache.is_echo(text="Hello", chat_guid="chat_001") is True def test_is_echo_none_params(self, cache): assert cache.is_echo(message_id=None, text=None) is False class TestLoopRateLimiter: @pytest.fixture def limiter(self): return LoopRateLimiter(max_loops=3, window_s=60.0, cooldown_s=1.0) def test_initial_not_suppressed(self, limiter): assert limiter.is_suppressed("conv_001") is False def test_single_hit_not_suppressed(self, limiter): result = limiter.check("conv_001", "content") assert result is False def test_multiple_hits_suppressed(self, limiter): for _ in range(3): limiter.check("conv_001", "content") result = limiter.check("conv_001", "content") assert result is True def test_different_conversation_separate(self, limiter): for _ in range(3): limiter.check("conv_001", "content") result = limiter.check("conv_002", "content") assert result is False def test_is_suppressed_after_hits(self, limiter): for _ in range(3): limiter.check("conv_001", "content") assert limiter.is_suppressed("conv_001") is True def test_cooldown_expires(self, limiter): limiter._cooldown_s = 0.01 for _ in range(3): limiter.check("conv_001", "content") assert limiter.is_suppressed("conv_001") is True time.sleep(0.02) assert limiter.is_suppressed("conv_001") is False def test_reset(self, limiter): for _ in range(2): limiter.check("conv_001", "content") limiter.reset("conv_001") result = limiter.check("conv_001", "content") assert result is False def test_reset_unknown_key(self, limiter): limiter.reset("nonexistent") def test_window_sliding(self, limiter): limiter._window_s = 0.01 for _ in range(2): limiter.check("conv_001", "content") time.sleep(0.02) result = limiter.check("conv_001", "content") assert result is False def test_default_params(self): limiter = LoopRateLimiter() assert limiter._max_loops == 5 assert limiter._window_s == 30.0 assert limiter._cooldown_s == 60.0 def test_check_within_window(self, limiter): for _ in range(2): limiter.check("conv_001", "content") result = limiter.check("conv_001", "content") assert result is True class TestReflectionGuard: @pytest.fixture def guard(self): return ReflectionGuard() def test_initial_state(self, guard): assert guard.blocked_count == 0 def test_is_not_reflection_normal_text(self, guard): assert guard.is_reflection("Hello world") is False def test_is_not_reflection_short_text(self, guard): assert guard.is_reflection("Hi") is False def test_mark_blocked(self, guard): guard.mark_blocked() assert guard.blocked_count == 1 guard.mark_blocked() assert guard.blocked_count == 2 def test_blocked_count_starts_at_zero(self, guard): assert guard.blocked_count == 0 guard.mark_blocked() guard.mark_blocked() assert guard.blocked_count == 2