343 lines
11 KiB
Python
343 lines
11 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from pydantic import ValidationError
|
||
|
|
|
||
|
|
from yuxi.channels.models import (
|
||
|
|
AgentRequest,
|
||
|
|
AgentResult,
|
||
|
|
Attachment,
|
||
|
|
ChannelAccountSnapshot,
|
||
|
|
ChannelIdentity,
|
||
|
|
ChannelMessage,
|
||
|
|
ChannelResponse,
|
||
|
|
ChannelStatus,
|
||
|
|
ChannelType,
|
||
|
|
ChatType,
|
||
|
|
DeliveryResult,
|
||
|
|
EventType,
|
||
|
|
HealthStatus,
|
||
|
|
MentionsInfo,
|
||
|
|
MessageType,
|
||
|
|
build_snapshot_from_adapter,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestMessageType:
|
||
|
|
def test_text_value(self):
|
||
|
|
assert MessageType.TEXT == "text"
|
||
|
|
|
||
|
|
def test_all_values_are_strings(self):
|
||
|
|
for member in MessageType:
|
||
|
|
assert isinstance(member.value, str)
|
||
|
|
|
||
|
|
|
||
|
|
class TestChannelType:
|
||
|
|
def test_webchat(self):
|
||
|
|
assert ChannelType.WEBCHAT == "webchat"
|
||
|
|
|
||
|
|
def test_has_wechat(self):
|
||
|
|
assert ChannelType.WECHAT == "wechat"
|
||
|
|
|
||
|
|
|
||
|
|
class TestChatType:
|
||
|
|
def test_direct(self):
|
||
|
|
assert ChatType.DIRECT == "direct"
|
||
|
|
|
||
|
|
def test_group(self):
|
||
|
|
assert ChatType.GROUP == "group"
|
||
|
|
|
||
|
|
|
||
|
|
class TestEventType:
|
||
|
|
def test_message_received(self):
|
||
|
|
assert EventType.MESSAGE_RECEIVED == "message.received"
|
||
|
|
|
||
|
|
|
||
|
|
class TestChannelIdentity:
|
||
|
|
def test_minimal_fields(self):
|
||
|
|
identity = ChannelIdentity(
|
||
|
|
channel_id="test_chan",
|
||
|
|
channel_type=ChannelType.WEBCHAT,
|
||
|
|
channel_user_id="user_123",
|
||
|
|
channel_chat_id="chat_456",
|
||
|
|
)
|
||
|
|
assert identity.channel_id == "test_chan"
|
||
|
|
assert identity.channel_type == ChannelType.WEBCHAT
|
||
|
|
assert identity.channel_message_id is None
|
||
|
|
|
||
|
|
def test_missing_required_field_raises(self):
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
ChannelIdentity(channel_id="test_chan", channel_type=ChannelType.WEBCHAT, channel_user_id="u1")
|
||
|
|
|
||
|
|
def test_all_fields(self):
|
||
|
|
identity = ChannelIdentity(
|
||
|
|
channel_id="telegram",
|
||
|
|
channel_type=ChannelType.TELEGRAM,
|
||
|
|
channel_user_id="@user",
|
||
|
|
channel_chat_id="12345",
|
||
|
|
channel_message_id="msg_001",
|
||
|
|
)
|
||
|
|
assert identity.channel_message_id == "msg_001"
|
||
|
|
|
||
|
|
|
||
|
|
class TestChannelMessage:
|
||
|
|
def _make_identity(self) -> ChannelIdentity:
|
||
|
|
return ChannelIdentity(
|
||
|
|
channel_id="test",
|
||
|
|
channel_type=ChannelType.WEBCHAT,
|
||
|
|
channel_user_id="u1",
|
||
|
|
channel_chat_id="c1",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_minimal_fields(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="hello")
|
||
|
|
assert msg.content == "hello"
|
||
|
|
assert msg.message_type == MessageType.TEXT
|
||
|
|
assert msg.event_type == EventType.MESSAGE_RECEIVED
|
||
|
|
assert msg.chat_type == ChatType.DIRECT
|
||
|
|
assert msg.attachments == []
|
||
|
|
assert msg.mentions is None
|
||
|
|
assert msg.extracted_urls == []
|
||
|
|
assert msg.metadata == {}
|
||
|
|
assert isinstance(msg.timestamp, datetime)
|
||
|
|
|
||
|
|
def test_with_mentions(self):
|
||
|
|
mentions = MentionsInfo(mentioned_user_ids=["u2"], is_bot_mentioned=True)
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="@bot hi", mentions=mentions)
|
||
|
|
assert msg.mentions.is_bot_mentioned is True
|
||
|
|
|
||
|
|
def test_with_attachments(self):
|
||
|
|
attachment = Attachment(type="image", url="https://example.com/img.png", filename="img.png")
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="image", attachments=[attachment])
|
||
|
|
assert len(msg.attachments) == 1
|
||
|
|
assert msg.attachments[0].type == "image"
|
||
|
|
|
||
|
|
def test_content_required(self):
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
ChannelMessage(identity=self._make_identity())
|
||
|
|
|
||
|
|
def test_identity_required(self):
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
ChannelMessage(content="hello")
|
||
|
|
|
||
|
|
def test_metadata_default(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="test")
|
||
|
|
assert msg.metadata == {}
|
||
|
|
|
||
|
|
def test_metadata_custom(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="test", metadata={"key": "val"})
|
||
|
|
assert msg.metadata["key"] == "val"
|
||
|
|
|
||
|
|
def test_timestamp_auto_generated(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="test")
|
||
|
|
assert isinstance(msg.timestamp, datetime)
|
||
|
|
|
||
|
|
def test_default_message_type(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="test")
|
||
|
|
assert msg.message_type == MessageType.TEXT
|
||
|
|
|
||
|
|
def test_custom_message_type(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="test", message_type=MessageType.IMAGE)
|
||
|
|
assert msg.message_type == MessageType.IMAGE
|
||
|
|
|
||
|
|
def test_reply_to_message_id(self):
|
||
|
|
msg = ChannelMessage(identity=self._make_identity(), content="test", reply_to_message_id="reply_123")
|
||
|
|
assert msg.reply_to_message_id == "reply_123"
|
||
|
|
|
||
|
|
|
||
|
|
class TestChannelResponse:
|
||
|
|
def _make_identity(self) -> ChannelIdentity:
|
||
|
|
return ChannelIdentity(
|
||
|
|
channel_id="test",
|
||
|
|
channel_type=ChannelType.WEBCHAT,
|
||
|
|
channel_user_id="u1",
|
||
|
|
channel_chat_id="c1",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_minimal_fields(self):
|
||
|
|
resp = ChannelResponse(identity=self._make_identity(), content="reply")
|
||
|
|
assert resp.content == "reply"
|
||
|
|
assert resp.message_type == MessageType.TEXT
|
||
|
|
assert isinstance(resp.timestamp, datetime)
|
||
|
|
|
||
|
|
def test_with_attachments(self):
|
||
|
|
attachment = Attachment(type="file", url="https://example.com/doc.pdf", filename="doc.pdf")
|
||
|
|
resp = ChannelResponse(identity=self._make_identity(), content="here", attachments=[attachment])
|
||
|
|
assert len(resp.attachments) == 1
|
||
|
|
assert resp.attachments[0].filename == "doc.pdf"
|
||
|
|
|
||
|
|
|
||
|
|
class TestDeliveryResult:
|
||
|
|
def test_success(self):
|
||
|
|
result = DeliveryResult(success=True, message_id="msg_123")
|
||
|
|
assert result.success is True
|
||
|
|
assert result.message_id == "msg_123"
|
||
|
|
assert result.error is None
|
||
|
|
|
||
|
|
def test_failure(self):
|
||
|
|
result = DeliveryResult(success=False, error="timeout")
|
||
|
|
assert result.success is False
|
||
|
|
assert result.error == "timeout"
|
||
|
|
|
||
|
|
def test_defaults(self):
|
||
|
|
result = DeliveryResult(success=True)
|
||
|
|
assert result.message_id is None
|
||
|
|
assert result.error is None
|
||
|
|
assert result.metadata == {}
|
||
|
|
|
||
|
|
|
||
|
|
class TestAgentRequest:
|
||
|
|
def test_minimal_fields(self):
|
||
|
|
identity = ChannelIdentity(
|
||
|
|
channel_id="test", channel_type=ChannelType.WEBCHAT, channel_user_id="u1", channel_chat_id="c1"
|
||
|
|
)
|
||
|
|
message = ChannelMessage(identity=identity, content="hello")
|
||
|
|
req = AgentRequest(user_id="user_1", thread_id="thread_1", agent_config_id=1, message=message)
|
||
|
|
assert req.user_id == "user_1"
|
||
|
|
assert req.thread_id == "thread_1"
|
||
|
|
assert req.agent_config_id == 1
|
||
|
|
assert req.agent_id is None
|
||
|
|
assert req.context == {}
|
||
|
|
|
||
|
|
|
||
|
|
class TestAgentResult:
|
||
|
|
def test_text_response(self):
|
||
|
|
result = AgentResult(response_text="hello world")
|
||
|
|
assert result.response_text == "hello world"
|
||
|
|
assert result.response_type == "text"
|
||
|
|
assert result.attachments == []
|
||
|
|
assert result.agent_state is None
|
||
|
|
|
||
|
|
def test_error_response(self):
|
||
|
|
result = AgentResult(response_text="error occurred", response_type="error")
|
||
|
|
assert result.response_type == "error"
|
||
|
|
|
||
|
|
def test_with_attachments(self):
|
||
|
|
attachment = Attachment(type="file", url="https://example.com/f.pdf")
|
||
|
|
result = AgentResult(response_text="here is file", attachments=[attachment])
|
||
|
|
assert len(result.attachments) == 1
|
||
|
|
|
||
|
|
|
||
|
|
class TestHealthStatus:
|
||
|
|
def test_healthy(self):
|
||
|
|
hs = HealthStatus(status="healthy", latency_ms=50.0)
|
||
|
|
assert hs.status == "healthy"
|
||
|
|
assert hs.latency_ms == 50.0
|
||
|
|
|
||
|
|
def test_degraded(self):
|
||
|
|
hs = HealthStatus(status="degraded", last_error="slow response")
|
||
|
|
assert hs.status == "degraded"
|
||
|
|
assert hs.last_error == "slow response"
|
||
|
|
|
||
|
|
def test_unhealthy(self):
|
||
|
|
hs = HealthStatus(status="unhealthy")
|
||
|
|
assert hs.status == "unhealthy"
|
||
|
|
|
||
|
|
def test_invalid_status_raises(self):
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
HealthStatus(status="unknown")
|
||
|
|
|
||
|
|
def test_default_metadata(self):
|
||
|
|
hs = HealthStatus(status="healthy")
|
||
|
|
assert hs.metadata == {}
|
||
|
|
|
||
|
|
|
||
|
|
class TestChannelStatus:
|
||
|
|
def test_disconnected(self):
|
||
|
|
assert ChannelStatus.DISCONNECTED == "disconnected"
|
||
|
|
|
||
|
|
def test_connected(self):
|
||
|
|
assert ChannelStatus.CONNECTED == "connected"
|
||
|
|
|
||
|
|
|
||
|
|
class TestMentionsInfo:
|
||
|
|
def test_defaults(self):
|
||
|
|
mentions = MentionsInfo()
|
||
|
|
assert mentions.mentioned_user_ids == []
|
||
|
|
assert mentions.is_bot_mentioned is False
|
||
|
|
assert mentions.raw_text is None
|
||
|
|
|
||
|
|
def test_bot_mentioned(self):
|
||
|
|
mentions = MentionsInfo(is_bot_mentioned=True)
|
||
|
|
assert mentions.is_bot_mentioned is True
|
||
|
|
|
||
|
|
|
||
|
|
class TestAttachment:
|
||
|
|
def test_defaults(self):
|
||
|
|
attachment = Attachment()
|
||
|
|
assert attachment.type == "file"
|
||
|
|
assert attachment.url is None
|
||
|
|
assert attachment.filename is None
|
||
|
|
|
||
|
|
def test_full_fields(self):
|
||
|
|
attachment = Attachment(
|
||
|
|
type="image",
|
||
|
|
url="https://example.com/photo.jpg",
|
||
|
|
filename="photo.jpg",
|
||
|
|
size_bytes=1024,
|
||
|
|
mime_type="image/jpeg",
|
||
|
|
file_id="file_001",
|
||
|
|
)
|
||
|
|
assert attachment.size_bytes == 1024
|
||
|
|
assert attachment.mime_type == "image/jpeg"
|
||
|
|
|
||
|
|
|
||
|
|
class TestChannelAccountSnapshot:
|
||
|
|
def test_default_values(self):
|
||
|
|
snap = ChannelAccountSnapshot()
|
||
|
|
assert snap.account_id == ""
|
||
|
|
assert snap.configured is False
|
||
|
|
assert snap.enabled is True
|
||
|
|
assert snap.linked is False
|
||
|
|
assert snap.running is False
|
||
|
|
assert snap.connected is False
|
||
|
|
assert snap.status_state == "not-configured"
|
||
|
|
assert snap.health_state == "stopped"
|
||
|
|
assert snap.dm_policy == "pairing"
|
||
|
|
assert snap.group_policy == "allowlist"
|
||
|
|
assert snap.reconnect_attempts == 0
|
||
|
|
|
||
|
|
def test_custom_values(self):
|
||
|
|
snap = ChannelAccountSnapshot(account_id="acc_1", name="test", configured=True, linked=True)
|
||
|
|
assert snap.account_id == "acc_1"
|
||
|
|
assert snap.name == "test"
|
||
|
|
assert snap.configured is True
|
||
|
|
assert snap.linked is True
|
||
|
|
|
||
|
|
def test_extra_fields_allowed(self):
|
||
|
|
snap = ChannelAccountSnapshot(account_id="acc_1", custom_field="extra_value")
|
||
|
|
assert snap.custom_field == "extra_value"
|
||
|
|
|
||
|
|
|
||
|
|
class TestBuildSnapshotFromAdapter:
|
||
|
|
def test_build_from_minimal_adapter(self):
|
||
|
|
class MinimalAdapter:
|
||
|
|
pass
|
||
|
|
|
||
|
|
adapter = MinimalAdapter()
|
||
|
|
snap = build_snapshot_from_adapter(adapter)
|
||
|
|
assert isinstance(snap, ChannelAccountSnapshot)
|
||
|
|
assert snap.status_state == "not-configured"
|
||
|
|
|
||
|
|
def test_build_from_full_adapter(self):
|
||
|
|
class FullAdapter:
|
||
|
|
account_id = "acc_full"
|
||
|
|
account_name = "Full Adapter"
|
||
|
|
_token_mgr = object()
|
||
|
|
_enabled = True
|
||
|
|
_linked = True
|
||
|
|
_running = True
|
||
|
|
_connected = True
|
||
|
|
_status_state = "active"
|
||
|
|
_status = None
|
||
|
|
|
||
|
|
adapter = FullAdapter()
|
||
|
|
snap = build_snapshot_from_adapter(adapter)
|
||
|
|
assert snap.account_id == "acc_full"
|
||
|
|
assert snap.name == "Full Adapter"
|
||
|
|
assert snap.configured is True
|
||
|
|
assert snap.linked is True
|
||
|
|
assert snap.running is True
|
||
|
|
assert snap.connected is True
|