ForcePilot/backend/test/unit/channels/test_msteams_storage.py
Kris 69fe97a90d test: 批量修复并新增单元测试用例
1. 移除Telegram格式化测试中未使用的导入项
2. 修复Teams测试用例,添加monkeypatch参数并配置通配符开关
3. 更新钉钉适配器测试,替换弃用的流属性检查
4. 修正Twitch规范化测试,更新ROOMSTATE测试逻辑
5. 重构会话映射测试,完善数据库执行结果模拟
6. 格式化Slack块构建测试的长参数调用
7. 修复LINE适配器测试,更新能力断言和异步锁使用
8. 修正Slack会话解析测试,修复聊天类型判断错误
9. 更新能力测试,补充缺失的字段检查
10. 修复Matrix适配器测试,修正位置参数和配置校验逻辑
11. 为飞书分析模块测试添加跳过标记
12. 新增微信能力、限流、链接格式、会话路由等模块的单元测试
13. 修复Twitch适配器导入路径和测试断言
14. 新增Discord Webhook、Nextcloud Talk、Signal多账户等模块的单元测试
15. 修复Manager阶段测试的导入路径
16. 新增iMessage异常和命令处理的单元测试
17. 新增Nostr健康检查和相关模块的单元测试
18. 新增Signal守护进程和SSE重连相关测试
2026-05-13 16:43:01 +08:00

496 lines
17 KiB
Python

"""MSTeams 存储类单元测试 (polls/pairing/feedback/sso/conv_store/sent_cache)。"""
from __future__ import annotations
import json
import time
from pathlib import Path
import pytest
from yuxi.channels.adapters.msteams.polls import PollStore
from yuxi.channels.adapters.msteams.pairing import PairingManager, build_pairing_request_card, _make_pair_id
from yuxi.channels.adapters.msteams.feedback import (
FeedbackLearningsStore,
process_feedback,
parse_reflection_response,
build_feedback_channel_data,
build_feedback_activity,
is_feedback_invoke,
parse_feedback_value,
build_reflection_prompt,
)
from yuxi.channels.adapters.msteams.sso import SSOTokenStore
from yuxi.channels.adapters.msteams.proactive import (
ConversationStore,
MemoryConversationStore,
extract_conversation_ref,
)
from yuxi.channels.adapters.msteams.sent_message_cache import SentMessageCache
class TestPollStore:
@pytest.fixture
def store(self, tmp_path):
return PollStore(storage_dir=str(tmp_path))
def test_create_poll(self, store):
poll = store.create_poll("poll-1", "Test Poll", ["A", "B", "C"])
assert poll["poll_id"] == "poll-1"
assert poll["title"] == "Test Poll"
assert len(poll["options"]) == 3
def test_create_poll_with_creator(self, store):
poll = store.create_poll("poll-2", "Poll", ["A", "B"], creator_id="user-1")
assert poll["creator_id"] == "user-1"
def test_create_poll_multi_select(self, store):
poll = store.create_poll("poll-3", "Poll", ["A", "B"], multi_select=True, max_selections=2)
assert poll["multi_select"] is True
assert poll["max_selections"] == 2
@pytest.mark.asyncio
async def test_cast_vote(self, store):
store.create_poll("poll-1", "Test", ["A", "B"])
result = await store.cast_vote("poll-1", "user-1", "A")
assert result is True
@pytest.mark.asyncio
async def test_cast_vote_invalid_option(self, store):
store.create_poll("poll-1", "Test", ["A", "B"])
result = await store.cast_vote("poll-1", "user-1", "C")
assert result is False
@pytest.mark.asyncio
async def test_cast_vote_poll_not_found(self, store):
result = await store.cast_vote("nonexistent", "user-1", "A")
assert result is False
@pytest.mark.asyncio
async def test_cast_vote_duplicate(self, store):
store.create_poll("poll-1", "Test", ["A", "B"])
await store.cast_vote("poll-1", "user-1", "A")
result = await store.cast_vote("poll-1", "user-1", "B")
assert result is False
def test_get_results(self, store):
store.create_poll("poll-1", "Test", ["A", "B"])
import asyncio
async def _vote():
await store.cast_vote("poll-1", "user-1", "A")
await store.cast_vote("poll-1", "user-2", "A")
await store.cast_vote("poll-1", "user-3", "B")
asyncio.run(_vote())
results = store.get_results("poll-1")
assert results is not None
assert results["total_votes"] == 3
assert results["tally"]["A"] == 2
assert results["tally"]["B"] == 1
def test_get_results_nonexistent(self, store):
assert store.get_results("nonexistent") is None
def test_get_poll(self, store):
store.create_poll("poll-1", "Test", ["A"])
poll = store.get_poll("poll-1")
assert poll is not None
assert poll["title"] == "Test"
def test_has_voted(self, store):
store.create_poll("poll-1", "Test", ["A"])
import asyncio
async def _vote():
await store.cast_vote("poll-1", "user-1", "A")
asyncio.run(_vote())
assert store.has_voted("poll-1", "user-1") is True
assert store.has_voted("poll-1", "user-2") is False
def test_active_count(self, store):
store.create_poll("poll-1", "Test", ["A"])
store.create_poll("poll-2", "Test", ["B"])
assert store.active_count == 2
def test_cleanup(self, store):
store.create_poll("poll-1", "Test", ["A"])
store.cleanup()
assert store.active_count == 1
class TestPairingManager:
@pytest.fixture
def manager(self, tmp_path):
return PairingManager(storage_dir=str(tmp_path))
def test_request_pairing(self, manager):
pair_id = manager.request_pairing("user-1", "Test User", "chat-1")
assert pair_id.startswith("req_")
assert pair_id.endswith(str(int(time.time())))
assert manager.is_pending("user-1") is True
def test_approve_pairing(self, manager):
pair_id = manager.request_pairing("user-1", "Test User")
result = manager.approve(pair_id)
assert result is True
assert manager.is_approved("user-1") is True
assert manager.is_pending("user-1") is False
def test_approve_nonexistent(self, manager):
result = manager.approve("nonexistent")
assert result is False
def test_deny_pairing(self, manager):
pair_id = manager.request_pairing("user-1", "Test User")
result = manager.deny(pair_id)
assert result is True
assert manager.is_approved("user-1") is False
assert manager.is_pending("user-1") is False
def test_deny_nonexistent(self, manager):
result = manager.deny("nonexistent")
assert result is False
def test_get_pending_requests(self, manager):
manager.request_pairing("user-1", "User One")
manager.request_pairing("user-2", "User Two")
pending = manager.get_pending_requests()
assert len(pending) == 2
def test_cleanup(self, manager):
pair_id = manager.request_pairing("user-1", "User")
count = manager.cleanup()
assert count > 0
def test_persistence(self, tmp_path):
mgr1 = PairingManager(storage_dir=str(tmp_path))
pair_id = mgr1.request_pairing("user-1", "Test")
mgr1.approve(pair_id)
mgr2 = PairingManager(storage_dir=str(tmp_path))
assert mgr2.is_approved("user-1") is True
class TestBuildPairingRequestCard:
def test_card_structure(self):
card = build_pairing_request_card("Test User", "user-001", "req_123")
assert card["type"] == "AdaptiveCard"
assert card["version"] == "1.5"
assert len(card["actions"]) == 2
assert card["actions"][0]["title"] == "批准"
assert card["actions"][1]["title"] == "拒绝"
class TestMakePairId:
def test_format(self):
result = _make_pair_id("user-001-abc", 1000000.0)
assert result.startswith("req_user-001-abc")
assert str(int(1000000)) in result
class TestFeedbackLearningsStore:
@pytest.fixture
def store(self, tmp_path):
return FeedbackLearningsStore(storage_dir=str(tmp_path))
def test_is_reflection_allowed_initial(self, store):
assert store.is_reflection_allowed("session-1") is True
def test_store_and_load_learnings(self, store):
store.store_session_learning("session-1", "user msg", "bot reply", "reflection text")
learnings = store.load_session_learnings("session-1")
assert len(learnings) == 1
assert learnings[0]["reflection"] == "reflection text"
def test_session_count(self, store):
store.store_session_learning("session-1", "msg", "reply", "reflect")
store.store_session_learning("session-2", "msg", "reply", "reflect")
assert store.session_count == 2
def test_clear(self, store):
store.store_session_learning("session-1", "msg", "reply", "reflect")
store.clear()
assert store.session_count == 0
def test_cooldown(self, store):
store.store_session_learning("session-1", "msg", "reply", "reflect")
assert store.is_reflection_allowed("session-1") is False
def test_clear_cooldowns(self, store):
store.store_session_learning("session-1", "msg", "reply", "reflect")
store.clear_reflection_cooldowns("session-1")
assert store.is_reflection_allowed("session-1") is True
def test_load_session_learnings_empty(self, store):
assert store.load_session_learnings("nonexistent") == []
class TestFeedbackFunctions:
def test_build_feedback_channel_data_disabled(self):
result = build_feedback_channel_data(feedback_enabled=False)
assert result is None
def test_build_feedback_channel_data_enabled(self):
result = build_feedback_channel_data(feedback_enabled=True)
assert result is not None
assert result["feedbackLoopEnabled"] is True
def test_build_feedback_channel_data_with_reflection(self):
result = build_feedback_channel_data(feedback_enabled=True, feedback_reflection=True)
assert result["feedbackReflection"] is True
def test_build_feedback_activity(self):
activity = build_feedback_activity("hello", feedback_enabled=True)
assert activity["text"] == "hello"
assert "channelData" in activity
def test_is_feedback_invoke(self):
assert is_feedback_invoke({"name": "message/submitAction"}) is True
assert is_feedback_invoke({"name": "other"}) is False
def test_parse_feedback_value(self):
result = parse_feedback_value({"value": {"feedbackValue": "like"}})
assert result == "like"
def test_parse_feedback_value_missing(self):
result = parse_feedback_value({})
assert result == ""
def test_build_reflection_prompt(self):
prompt = build_reflection_prompt("user text", "bot text")
assert "user text" in prompt
assert "bot text" in prompt
assert "👎" in prompt
def test_parse_reflection_json(self):
raw = json.dumps({"reflection": "I should improve"})
result = parse_reflection_response(raw)
assert result == "I should improve"
def test_parse_reflection_code_block(self):
raw = '```json\n{"reflection": "from code"}\n```'
result = parse_reflection_response(raw)
assert result == "from code"
def test_parse_reflection_fallback(self):
raw = "plain text reflection"
result = parse_reflection_response(raw)
assert result == "plain text reflection"
class TestProcessFeedback:
def test_positive_feedback(self):
activity = {
"name": "message/submitAction",
"value": {"feedbackValue": "positive"},
"from": {"id": "user-1", "name": "Test", "aadObjectId": "aad-1"},
}
result = process_feedback(activity)
assert result["is_positive"] is True
assert result["is_negative"] is False
assert result["user_id"] == "aad-1"
def test_negative_feedback(self):
activity = {
"name": "message/submitAction",
"value": {"feedbackValue": "negative"},
"from": {"id": "user-1", "name": "Test", "aadObjectId": "aad-1"},
}
result = process_feedback(activity)
assert result["is_positive"] is False
assert result["is_negative"] is True
def test_unknown_feedback(self):
activity = {
"name": "message/submitAction",
"value": {"feedbackValue": "other"},
"from": {"id": "user-1", "aadObjectId": "aad-1"},
}
result = process_feedback(activity)
assert result["is_positive"] is False
assert result["is_negative"] is False
def test_fallback_user_id(self):
activity = {
"name": "message/submitAction",
"value": {"feedbackValue": "positive"},
"from": {"id": "user-1"},
}
result = process_feedback(activity)
assert result["user_id"] == "user-1"
def test_reply_to_id(self):
activity = {
"name": "message/submitAction",
"value": {"feedbackValue": "positive"},
"from": {"id": "user-1"},
"replyToId": "msg-1",
}
result = process_feedback(activity)
assert result["reply_to_id"] == "msg-1"
class TestSSOTokenStore:
@pytest.fixture
def store(self, tmp_path):
return SSOTokenStore(storage_dir=str(tmp_path))
def test_store_and_get(self, store):
store.store("user-1", {"token": "abc123", "expires": "2024-01-01"})
result = store.get("user-1")
assert result is not None
assert result["token"] == "abc123"
def test_get_nonexistent(self, store):
result = store.get("nonexistent")
assert result is None
def test_remove(self, store):
store.store("user-1", {"token": "abc"})
store.remove("user-1")
assert store.get("user-1") is None
def test_clear(self, store):
store.store("user-1", {"token": "abc"})
store.store("user-2", {"token": "def"})
store.clear()
assert store.get("user-1") is None
assert store.get("user-2") is None
def test_persistence(self, tmp_path):
store1 = SSOTokenStore(storage_dir=str(tmp_path))
store1.store("user-1", {"token": "abc"})
store2 = SSOTokenStore(storage_dir=str(tmp_path))
result = store2.get("user-1")
assert result is not None
class TestConversationStore:
@pytest.fixture
def store(self, tmp_path):
return ConversationStore(storage_dir=str(tmp_path))
def test_store_and_get(self, store):
store.store("chat-1", {"conversation": {"id": "conv-1"}})
ref = store.get("chat-1")
assert ref is not None
assert ref["conversation"]["id"] == "conv-1"
def test_get_nonexistent(self, store):
assert store.get("nonexistent") is None
def test_remove(self, store):
store.store("chat-1", {"data": "test"})
store.remove("chat-1")
assert store.get("chat-1") is None
def test_list_keys(self, store):
store.store("chat-1", {"data": "a"})
store.store("chat-2", {"data": "b"})
keys = store.list_keys()
assert "chat-1" in keys
assert "chat-2" in keys
def test_count(self, store):
store.store("chat-1", {"data": "a"})
assert store.count == 1
def test_persistence(self, tmp_path):
store1 = ConversationStore(storage_dir=str(tmp_path))
store1.store("chat-1", {"data": "test"})
store2 = ConversationStore(storage_dir=str(tmp_path))
assert store2.get("chat-1") is not None
class TestMemoryConversationStore:
@pytest.fixture
def store(self):
return MemoryConversationStore()
def test_store_and_get(self, store):
store.store("chat-1", {"data": "test"})
assert store.get("chat-1") is not None
assert store.get("chat-1")["data"] == "test"
def test_get_nonexistent(self, store):
assert store.get("nonexistent") is None
def test_remove(self, store):
store.store("chat-1", {"data": "test"})
store.remove("chat-1")
assert store.get("chat-1") is None
def test_count(self, store):
store.store("a", {"d": 1})
store.store("b", {"d": 2})
assert store.count == 2
class TestExtractConversationRef:
def test_basic_extraction(self):
activity = {
"id": "activity-1",
"from": {"id": "user-1", "name": "Test"},
"recipient": {"id": "bot-1", "name": "Bot"},
"conversation": {"id": "conv-1"},
"channelId": "msteams",
"serviceUrl": "https://example.com",
"channelData": {"tenant": {"id": "tenant-1"}},
}
ref = extract_conversation_ref(activity)
assert ref["activityId"] == "activity-1"
assert ref["user"]["id"] == "user-1"
assert ref["bot"]["id"] == "bot-1"
assert ref["graphChatId"] == "conv-1"
assert ref["tenantId"] == "tenant-1"
assert ref["serviceUrl"] == "https://example.com"
def test_missing_fields_default(self):
activity = {"id": "a1", "from": {}, "conversation": {}}
ref = extract_conversation_ref(activity)
assert ref["activityId"] == "a1"
assert ref["graphChatId"] == ""
class TestSentMessageCache:
@pytest.fixture
def cache(self):
return SentMessageCache()
def test_record_and_was_sent(self, cache):
cache.record("msg-1", "chat-1")
assert cache.was_sent("msg-1") is True
def test_was_sent_nonexistent(self, cache):
assert cache.was_sent("nonexistent") is False
def test_get_chat_id(self, cache):
cache.record("msg-1", "chat-1")
assert cache.get_chat_id("msg-1") == "chat-1"
def test_get_chat_id_nonexistent(self, cache):
assert cache.get_chat_id("nonexistent") is None
def test_count(self, cache):
cache.record("msg-1", "chat-1")
cache.record("msg-2", "chat-2")
assert cache.count == 2
def test_clear(self, cache):
cache.record("msg-1", "chat-1")
cache.clear()
assert cache.count == 0
def test_lru_eviction(self):
cache = SentMessageCache(max_entries=3)
for i in range(5):
cache.record(f"msg-{i}", f"chat-{i}")
assert cache.count <= 3