新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
224 lines
7.8 KiB
Python
224 lines
7.8 KiB
Python
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 |