ForcePilot/backend/test/unit/channels/test_yuanbao_optimization.py

325 lines
11 KiB
Python
Raw Normal View History

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.yuanbao.adapter import YuanbaoAdapter
from yuxi.channels.adapters.yuanbao.dispatch import (
DispatchAction,
InteractiveDispatcher,
)
from yuxi.channels.adapters.yuanbao.format import format_outbound, _map_message_type
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelType,
ChatType,
DeliveryResult,
MessageType,
)
YUANBAO = ChannelType.YUANBAO
def _make_identity(
channel_chat_id: str = "user_openid_001",
channel_user_id: str = "user_openid_001",
channel_message_id: str = "msg_001",
) -> ChannelIdentity:
return ChannelIdentity(
channel_id="yuanbao",
channel_type=YUANBAO,
channel_user_id=channel_user_id,
channel_chat_id=channel_chat_id,
channel_message_id=channel_message_id,
)
def _make_adapter(config: dict | None = None) -> YuanbaoAdapter:
cfg = config or {"app_key": "test_key", "app_secret": "test_secret", "dm_policy": "open"}
return YuanbaoAdapter(config=cfg)
# ==================== ChannelCapabilities Tests ====================
class TestChannelCapabilitiesOptimization:
def test_capabilities_has_explicit_false_fields(self):
adapter = _make_adapter()
caps = adapter.capabilities
assert caps.edit is False
assert caps.unsend is False
assert caps.pin is False
assert caps.polls is False
assert caps.typing is False
assert caps.send_ephemeral is False
assert caps.effects is False
def test_capabilities_retains_true_fields(self):
adapter = _make_adapter()
caps = adapter.capabilities
assert caps.reply is True
assert caps.reactions is True
assert caps.media is True
assert caps.block_streaming is True
assert caps.lane_streaming is True
assert caps.reasoning_streaming is True
assert caps.native_commands is True
assert caps.supports_markdown is True
assert caps.supports_streaming is True
def test_capabilities_chat_types(self):
adapter = _make_adapter()
caps = adapter.capabilities
assert "direct" in caps.chat_types
assert "group" in caps.chat_types
def test_capabilities_streaming_modes(self):
adapter = _make_adapter()
caps = adapter.capabilities
assert "off" in caps.streaming_modes
assert "block" in caps.streaming_modes
assert "lane" in caps.streaming_modes
assert "reasoning" in caps.streaming_modes
# ==================== send_typing_indicator Tests ====================
class TestSendTypingIndicator:
@pytest.fixture
def adapter(self):
return _make_adapter()
@pytest.mark.asyncio
async def test_typing_indicator_no_client(self, adapter):
result = await adapter.send_typing_indicator("chat_001")
assert result.success is False
assert "not initialized" in result.error.lower()
@pytest.mark.asyncio
async def test_typing_indicator_success(self, adapter):
adapter._http_client = MagicMock()
adapter._token_manager = MagicMock()
adapter._token_manager.get_token = AsyncMock(return_value="test_token")
adapter._token_manager.api_base = "https://api.yuanbao.test"
mock_resp = AsyncMock()
mock_resp.status = 200
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
mock_resp.__aexit__ = AsyncMock(return_value=None)
adapter._http_client.post.return_value = mock_resp
result = await adapter.send_typing_indicator("open_id_001")
assert result.success is True
@pytest.mark.asyncio
async def test_typing_indicator_http_error(self, adapter):
adapter._http_client = MagicMock()
adapter._token_manager = MagicMock()
adapter._token_manager.get_token = AsyncMock(return_value="test_token")
adapter._token_manager.api_base = "https://api.yuanbao.test"
mock_resp = AsyncMock()
mock_resp.status = 500
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
mock_resp.__aexit__ = AsyncMock(return_value=None)
adapter._http_client.post.return_value = mock_resp
result = await adapter.send_typing_indicator("open_id_001")
assert result.success is False
assert "500" in result.error
@pytest.mark.asyncio
async def test_typing_indicator_network_exception(self, adapter):
adapter._http_client = MagicMock()
adapter._token_manager = MagicMock()
adapter._token_manager.get_token = AsyncMock(return_value="test_token")
adapter._token_manager.api_base = "https://api.yuanbao.test"
adapter._http_client.post.side_effect = Exception("Connection refused")
result = await adapter.send_typing_indicator("open_id_001")
assert result.success is False
assert "Connection refused" in result.error
# ==================== edit_message / delete_message Tests ====================
class TestEditDeleteNoSideEffects:
@pytest.fixture
def adapter(self):
return _make_adapter()
@pytest.mark.asyncio
async def test_edit_message_no_cache_side_effect(self, adapter):
adapter._send_cache.add("msg_abc", "chat_123", "Original content")
result = await adapter.edit_message("chat_123", "msg_abc", "Updated content")
assert result.success is False
assert "not supported" in result.error.lower()
entry = adapter._send_cache.get("msg_abc")
assert entry is not None
assert entry.status == "sent"
@pytest.mark.asyncio
async def test_delete_message_no_cache_side_effect(self, adapter):
adapter._send_cache.add("msg_abc", "chat_123", "Original content")
adapter._bot_message_ids.add("msg_abc")
result = await adapter.delete_message("chat_123", "msg_abc")
assert result.success is False
assert "not supported" in result.error.lower()
entry = adapter._send_cache.get("msg_abc")
assert entry is not None
assert entry.status == "sent"
assert "msg_abc" in adapter._bot_message_ids
# ==================== SYSTEM_EVENT Handler Tests ====================
class TestSystemEventHandler:
@pytest.fixture
def adapter(self):
return _make_adapter()
@pytest.mark.asyncio
async def test_handle_system_event_typing(self, adapter):
event = {
"type": "typing",
"open_id": "user_123",
"group_open_id": "group_abc",
}
await adapter._handle_system_event(event)
@pytest.mark.asyncio
async def test_handle_system_event_unknown(self, adapter):
event = {
"type": "some_custom_event",
"open_id": "user_123",
}
await adapter._handle_system_event(event)
@pytest.mark.asyncio
async def test_handle_ws_event_dispatches_system_event(self, adapter):
event = {
"type": "typing",
"msg_id": "msg_typing_001",
"open_id": "user_123",
}
adapter._seen_message_ids.clear()
adapter._handle_system_event = AsyncMock()
await adapter._handle_ws_event(event)
adapter._handle_system_event.assert_called_once_with(event)
# ==================== LOCATION Type Mapping Tests ====================
class TestLocationTypeMapping:
def test_map_message_type_includes_location(self):
result = _map_message_type(MessageType.LOCATION, "")
assert result == "location"
def test_format_outbound_with_location_type(self):
identity = _make_identity(channel_chat_id="user_001")
response = ChannelResponse(
identity=identity,
content="Location share",
message_type=MessageType.LOCATION,
)
payload = format_outbound(response)
assert payload["msg_type"] == "location"
assert payload["content"] == "Location share"
# ==================== Dispatch Classification Tests ====================
class TestDispatchClassificationOptimization:
def test_channel_post_classified_as_message(self):
event = {
"type": "channel_post",
"open_id": "user_123",
"channel_id": "ch_001",
"content": "Channel broadcast",
}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.MESSAGE
def test_edited_channel_post_classified_as_message(self):
event = {
"type": "edited_channel_post",
"open_id": "user_123",
"channel_id": "ch_001",
"content": "Edited channel broadcast",
}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.MESSAGE
def test_normal_message_still_classified_correctly(self):
event = {
"type": "message",
"open_id": "user_123",
"content": "Hello",
}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.MESSAGE
def test_command_still_classified_correctly(self):
event = {
"type": "message",
"open_id": "user_123",
"content": "/start",
}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.COMMAND
def test_edited_message_still_classified_correctly(self):
event = {
"type": "edited_message",
"open_id": "user_123",
"content": "Updated",
}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.MESSAGE
def test_reaction_still_classified_correctly(self):
event = {"type": "reaction_added", "open_id": "user_123"}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.REACTION
def test_bot_menu_still_classified_correctly(self):
event = {"type": "bot_menu", "open_id": "user_123"}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.BOT_MENU
def test_member_event_still_classified_correctly(self):
event = {"type": "member_joined", "open_id": "user_123"}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.MEMBER_EVENT
def test_card_action_still_classified_correctly(self):
event = {"type": "card_action", "open_id": "user_123"}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.CARD_ACTION
def test_read_receipt_still_classified_correctly(self):
event = {"type": "read_receipt", "open_id": "user_123"}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.READ_RECEIPT
def test_unknown_event_type(self):
event = {"type": "unknown_type", "open_id": "user_123"}
action = InteractiveDispatcher.classify_event(event)
assert action == DispatchAction.UNKNOWN