ForcePilot/backend/test/unit/channels/test_channels_matrix.py
Kris fbe47b6393 refactor(webhook,tests): 完善webhook日志与多适配器测试修复
1.  webhook路由新增记录请求源IP与请求头
2.  修复多个适配器单元测试:更新配置断言、补全async测试装饰器、重构测试调用逻辑
3.  新增Nostr适配器测试通用fixture
4.  更新NextcloudTalk签名相关测试与函数命名
2026-05-12 14:52:12 +08:00

2568 lines
94 KiB
Python

"""Unit tests for Matrix channel adapter.
Tests formatter, normalizer, thread_utils, session, crypto, probe, adapter,
and new modules: reaction_utils, encryption, concurrency, config_patch,
group_mentions, profile_sync, session (S7 target ID), probe (S14 device/backup).
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.matrix.adapter import MatrixAdapter
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
from yuxi.channels.adapters.matrix.session import MatrixSessionHelper
from yuxi.channels.adapters.matrix.thread_utils import is_thread_event
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelStatus,
ChannelType,
ChatType,
DeliveryResult,
EventType,
MessageType,
)
CHANNEL_ID = "matrix"
CHANNEL_TYPE = ChannelType.MATRIX
class TestFormatter:
def test_bold_text(self):
result = markdown_to_matrix_html("**hello**")
assert "<b>hello</b>" in result
def test_italic_text(self):
result = markdown_to_matrix_html("*hello*")
assert "<i>hello</i>" in result
def test_strikethrough_text(self):
result = markdown_to_matrix_html("~~hello~~")
assert "<del>hello</del>" in result
def test_inline_code(self):
result = markdown_to_matrix_html("`code`")
assert "<code>code</code>" in result
def test_code_block_escapes_html(self):
result = markdown_to_matrix_html("```\n<script>alert(1)</script>\n```")
assert "<pre><code>" in result
assert "&lt;script&gt;" in result
assert "alert(1)" in result
assert "&lt;/script&gt;" in result
def test_link_renders_anchor(self):
result = markdown_to_matrix_html("[click](https://example.com)")
assert '<a href="https://example.com">' in result
def test_heading_renders_h_tags(self):
result = markdown_to_matrix_html("## Hello")
assert "<h2>Hello</h2>" in result
def test_horizontal_rule_renders_hr(self):
result = markdown_to_matrix_html("---")
assert "<hr>" in result
def test_unordered_list_renders_li(self):
result = markdown_to_matrix_html("- item")
assert "<li>item</li>" in result
def test_ordered_list_renders_li(self):
result = markdown_to_matrix_html("1. item")
assert "<li>item</li>" in result
def test_blockquote_renders_blockquote(self):
result = markdown_to_matrix_html("> quoted")
assert "<blockquote>quoted</blockquote>" in result
def test_strips_script_tag(self):
result = markdown_to_matrix_html("<script>alert(1)</script>")
assert "<script>" not in result
def test_strips_iframe_tag(self):
result = markdown_to_matrix_html("<iframe src='x'></iframe>")
assert "<iframe" not in result
def test_strips_svg_tag(self):
result = markdown_to_matrix_html("<svg></svg>")
assert "<svg" not in result
def test_strips_event_handler_attrs(self):
result = markdown_to_matrix_html("<img onerror=alert(1)>")
assert "onerror" not in result
def test_strips_javascript_href(self):
result = markdown_to_matrix_html('<a href="javascript:alert(1)">')
assert "javascript:" not in result
def test_strips_style_tag(self):
result = markdown_to_matrix_html("<style>body{}</style>")
assert "<style" not in result
def test_plain_text_unchanged(self):
result = markdown_to_matrix_html("hello world")
assert "hello world" in result
def test_underscore_bold(self):
result = markdown_to_matrix_html("__hello__")
assert "<b>hello</b>" in result
def test_underscore_italic(self):
result = markdown_to_matrix_html("_hello_")
assert "<i>hello</i>" in result
def test_combined_formatting(self):
result = markdown_to_matrix_html("**bold** and *italic* and ~~strike~~ and `code`")
assert "<b>bold</b>" in result
assert "<i>italic</i>" in result
assert "<del>strike</del>" in result
assert "<code>code</code>" in result
def test_multiline_text_uses_br(self):
result = markdown_to_matrix_html("line1\nline2")
assert "<br>" in result
class TestThreadUtils:
def test_thread_event_with_rel_type(self):
event = MagicMock()
event.source = {
"content": {
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$event123",
}
}
}
is_thread, root_id = is_thread_event(event)
assert is_thread is True
assert root_id == "$event123"
def test_non_thread_event(self):
event = MagicMock()
event.source = {
"content": {
"m.relates_to": {
"m.in_reply_to": {"event_id": "$event456"},
}
}
}
is_thread, root_id = is_thread_event(event)
assert is_thread is False
assert root_id is None
def test_empty_content(self):
event = MagicMock()
event.source = {}
is_thread, root_id = is_thread_event(event)
assert is_thread is False
assert root_id is None
class TestSessionHelper:
@pytest.fixture
def adapter(self):
return MatrixAdapter({"user_id": "@bot:example.org"})
def test_resolve_chat_id_dm(self, adapter):
sid = adapter._session.resolve_chat_id("!room:example.org", is_dm=True)
assert sid == "dm_!room:example.org"
def test_resolve_chat_id_group(self, adapter):
sid = adapter._session.resolve_chat_id("!room:example.org", is_dm=False)
assert sid == "!room:example.org"
def test_resolve_thread_id_dm(self, adapter):
tid = adapter._session.resolve_thread_id("!room:example.org", is_dm=True, is_thread=False, root_event_id=None)
assert "dm" in tid
assert "main" in tid
def test_resolve_thread_id_group(self, adapter):
tid = adapter._session.resolve_thread_id("!room:example.org", is_dm=False, is_thread=False, root_event_id=None)
assert "room" in tid
def test_resolve_thread_id_with_thread(self, adapter):
tid = adapter._session.resolve_thread_id(
"!room:example.org", is_dm=False, is_thread=True, root_event_id="$evt", agent_config_id=5
)
assert "thread" in tid
assert "agent:5" in tid
assert "$evt" in tid
def test_resolve_chat_type_direct(self, adapter):
adapter._direct_rooms.add("!dm:example.org")
assert adapter._session.resolve_chat_type("!dm:example.org") == "direct"
def test_resolve_chat_type_group(self, adapter):
assert adapter._session.resolve_chat_type("!room:example.org") == "group"
class TestNormalizer:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({
"user_id": "@bot:example.org",
"homeserver": "https://example.org",
"requireMention": False,
})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
a._direct_rooms.add("!dm:example.org")
return a
def _make_text_event(self, sender="", body="", formatted_body=""):
from nio import RoomMessageText
event = MagicMock(spec=RoomMessageText)
event.sender = sender
event.body = body
event.event_id = "$evt001"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"msgtype": "m.text",
"body": body,
"formatted_body": formatted_body,
}
}
return event
def _make_image_event(self, sender=""):
from nio import RoomMessageImage
event = MagicMock(spec=RoomMessageImage)
event.sender = sender
event.body = "image.png"
event.event_id = "$evt002"
event.server_timestamp = 1700000001000
event.source = {
"content": {
"msgtype": "m.image",
"body": "image.png",
"url": "mxc://example.org/img001",
"info": {"mimetype": "image/png"},
}
}
return event
def test_dm_text_message(self, adapter):
event = self._make_text_event(sender="@user:example.org", body="Hello")
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is not None
assert msg.message_type == MessageType.TEXT
assert msg.content == "Hello"
assert msg.identity.channel_user_id == "@user:example.org"
def test_skips_own_message(self, adapter):
event = self._make_text_event(sender="@bot:example.org", body="Hi")
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is None
def test_image_message(self, adapter):
event = self._make_image_event(sender="@user:example.org")
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is not None
assert msg.message_type == MessageType.IMAGE
assert len(msg.attachments) == 1
assert msg.attachments[0].type == "image"
assert msg.attachments[0].url == "mxc://example.org/img001"
def test_bot_mention_from_formatted_body(self, adapter):
formatted = f'<a href="https://matrix.to/#/{adapter.user_id}">bot</a>'
event = self._make_text_event(sender="@user:example.org", body="hey", formatted_body=formatted)
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.mentions is not None
assert msg.mentions.is_bot_mentioned is True
assert msg.mentions.mentioned_user_ids
def test_extract_reply_to(self, adapter):
event = MagicMock()
event.sender = "@user:example.org"
event.body = "reply"
event.event_id = "$evt003"
event.server_timestamp = 1700000002000
event.source = {
"content": {
"body": "reply",
"m.relates_to": {
"m.in_reply_to": {"event_id": "$original"},
},
}
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.reply_to_message_id == "$original"
def test_plain_text_mention_extraction(self, adapter):
event = self._make_text_event(sender="@user:example.org", body="@alice:matrix.org hello")
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.mentions is not None
assert any("alice" in uid for uid in msg.mentions.mentioned_user_ids)
def test_content_type_audio(self, adapter):
from nio import RoomMessageAudio
event = MagicMock(spec=RoomMessageAudio)
event.sender = "@user:example.org"
event.body = "audio.mp3"
event.event_id = "$evt004"
event.server_timestamp = 1700000003000
event.source = {
"content": {
"msgtype": "m.audio",
"body": "audio.mp3",
"url": "mxc://example.org/audio001",
"info": {"mimetype": "audio/mp3"},
}
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.message_type == MessageType.AUDIO
assert msg.attachments[0].type == "audio"
def test_content_type_video(self, adapter):
from nio import RoomMessageVideo
event = MagicMock(spec=RoomMessageVideo)
event.sender = "@user:example.org"
event.body = "video.mp4"
event.event_id = "$evt005"
event.server_timestamp = 1700000004000
event.source = {
"content": {
"msgtype": "m.video",
"body": "video.mp4",
"url": "mxc://example.org/video001",
"info": {"mimetype": "video/mp4"},
}
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.message_type == MessageType.VIDEO
def test_content_type_file(self, adapter):
from nio import RoomMessageFile
event = MagicMock(spec=RoomMessageFile)
event.sender = "@user:example.org"
event.body = "doc.pdf"
event.event_id = "$evt006"
event.server_timestamp = 1700000005000
event.source = {
"content": {
"msgtype": "m.file",
"body": "doc.pdf",
"url": "mxc://example.org/file001",
"info": {"mimetype": "application/pdf"},
}
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.message_type == MessageType.FILE
assert msg.attachments[0].filename == "doc.pdf"
class TestAdapterStatus:
def test_initial_status_disconnected(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter.status == "disconnected"
def test_status_after_connect_starts_connecting(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter._status == ChannelStatus.DISCONNECTED
adapter._status = ChannelStatus.CONNECTING
assert adapter.status == "connecting"
def test_stream_state_initialized_empty(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter._stream_state == {}
def test_circuit_breaker_initialized(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter._circuit_breaker is not None
def test_disconnect_clears_stream_state(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
adapter._stream_state["key"] = 100
adapter._client = MagicMock()
adapter._client.close = AsyncMock()
import asyncio
asyncio.get_event_loop().run_until_complete(adapter.disconnect())
assert adapter._stream_state == {}
def test_user_id_returns_empty_when_no_client(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter.user_id == ""
def test_user_id_returns_client_user_id(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
adapter._client = MagicMock()
adapter._client.user_id = "@bot:example.org"
assert adapter.user_id == "@bot:example.org"
class TestAdapterSend:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_send_media_encodes_non_bytes(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
result = await adapter.send_media("!room:example.org", "image", "data_string")
assert result.success is True
@pytest.mark.asyncio
async def test_send_reaction_invokes_send_module(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
result = await adapter.send_reaction("!room:example.org", "$msg", "👍")
assert result.success is True
class TestAdapterRoomManagement:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_invite_user_success(self, adapter):
mock_resp = MagicMock()
mock_resp.event_id = "$evt001"
adapter._circuit_breaker.call = AsyncMock(return_value=DeliveryResult(success=True, message_id="$evt001"))
result = await adapter.invite_user("!room:example.org", "@user:example.org")
assert result.success is True
@pytest.mark.asyncio
async def test_kick_user_success(self, adapter):
mock_resp = MagicMock()
mock_resp.event_id = "$evt002"
adapter._circuit_breaker.call = AsyncMock(return_value=DeliveryResult(success=True, message_id="$evt002"))
result = await adapter.kick_user("!room:example.org", "@user:example.org", "spam")
assert result.success is True
@pytest.mark.asyncio
async def test_ban_user_success(self, adapter):
mock_resp = MagicMock()
mock_resp.event_id = "$evt003"
adapter._circuit_breaker.call = AsyncMock(return_value=DeliveryResult(success=True, message_id="$evt003"))
result = await adapter.ban_user("!room:example.org", "@user:example.org", "abuse")
assert result.success is True
@pytest.mark.asyncio
async def test_leave_room_success(self, adapter):
adapter._circuit_breaker.call = AsyncMock(return_value=DeliveryResult(success=True, message_id="$evt004"))
result = await adapter.leave_room("!room:example.org")
assert result.success is True
@pytest.mark.asyncio
async def test_join_room_success(self, adapter):
adapter._circuit_breaker.call = AsyncMock(return_value=DeliveryResult(success=True, message_id="$evt005"))
result = await adapter.join_room("!room:example.org")
assert result.success is True
@pytest.mark.asyncio
async def test_invite_user_no_client_returns_error(self, adapter):
adapter._client = None
result = await adapter.invite_user("!room", "@user")
assert result.success is False
assert result.error == "Client not initialized"
class TestAdapterTypingAndReceipt:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_send_typing_indicator_calls_client(self, adapter):
adapter._client.room_typing = AsyncMock()
await adapter.send_typing_indicator("!room:example.org", typing=True)
adapter._client.room_typing.assert_called_once()
@pytest.mark.asyncio
async def test_send_typing_indicator_no_client(self, adapter):
adapter._client = None
await adapter.send_typing_indicator("!room:example.org")
@pytest.mark.asyncio
async def test_send_read_receipt_calls_client(self, adapter):
adapter._client.room_read_markers = AsyncMock()
await adapter.send_read_receipt("!room:example.org", "$evt001")
adapter._client.room_read_markers.assert_called_once()
@pytest.mark.asyncio
async def test_send_read_receipt_no_client(self, adapter):
adapter._client = None
await adapter.send_read_receipt("!room:example.org", "$evt001")
class TestAdapterNoClientReturnsError:
@pytest.fixture
def adapter_no_client(self):
return MatrixAdapter({"user_id": "@bot:example.org"})
@pytest.mark.asyncio
async def test_send_no_client(self, adapter_no_client):
identity = ChannelIdentity(
channel_id="matrix",
channel_type=ChannelType.MATRIX,
channel_user_id="",
channel_chat_id="!room",
channel_message_id="$msg",
)
response = ChannelResponse(identity=identity, content="hello")
result = await adapter_no_client.send(response)
assert result.success is False
assert result.error == "Client not initialized"
@pytest.mark.asyncio
async def test_send_media_no_client(self, adapter_no_client):
result = await adapter_no_client.send_media("!room", "image", b"data")
assert result.success is False
@pytest.mark.asyncio
async def test_edit_message_no_client(self, adapter_no_client):
result = await adapter_no_client.edit_message("!room", "$msg", "edited")
assert result.success is False
@pytest.mark.asyncio
async def test_delete_message_no_client(self, adapter_no_client):
result = await adapter_no_client.delete_message("!room", "$msg")
assert result.success is False
@pytest.mark.asyncio
async def test_get_user_info_no_client(self, adapter_no_client):
result = await adapter_no_client.get_user_info("@user:matrix.org")
assert result == {}
class TestClassifySendError:
def test_auth_error_classified(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception()
exc.status_code = 403
exc.errcode = "M_UNKNOWN_TOKEN"
error_type, retry_after = classify_send_error(exc)
assert error_type == "auth"
assert retry_after is None
def test_forbidden_error_also_auth(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception()
exc.status_code = 403
exc.errcode = "M_FORBIDDEN"
error_type, _ = classify_send_error(exc)
assert error_type == "auth"
def test_rate_limit_classified(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception()
exc.status_code = 429
exc.retry_after_ms = 3000
error_type, retry_after = classify_send_error(exc)
assert error_type == "rate_limit"
assert retry_after == 3000
def test_rate_limit_default_backoff(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception()
exc.status_code = 429
error_type, retry_after = classify_send_error(exc)
assert error_type == "rate_limit"
assert retry_after == 5000
def test_unknown_error_is_other(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception("random error")
error_type, _ = classify_send_error(exc)
assert error_type == "other"
def test_no_status_code_is_other(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception()
error_type, _ = classify_send_error(exc)
assert error_type == "other"
class TestSendWithAuthRetry:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org", "password": "pwd"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_send_retries_on_auth_error(self, adapter):
from yuxi.channels.infra.circuit_breaker import CircuitBreakerOpenError
call_count = 0
async def _fail_then_ok(*_args, **_kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
exc = Exception("token expired")
exc.status_code = 403
exc.errcode = "M_UNKNOWN_TOKEN"
raise exc
return DeliveryResult(success=True, message_id="$ok")
adapter._circuit_breaker.call = _fail_then_ok
adapter._refresh_token_if_needed = AsyncMock(return_value=True)
result = await adapter._send_with_circuit(lambda: DeliveryResult(success=True))
assert result.success is True
assert call_count == 2
adapter._refresh_token_if_needed.assert_awaited_once()
@pytest.mark.asyncio
async def test_send_fails_if_token_refresh_fails(self, adapter):
call_count = 0
async def _always_fail(*_args, **_kwargs):
nonlocal call_count
call_count += 1
exc = Exception("token expired")
exc.status_code = 403
exc.errcode = "M_UNKNOWN_TOKEN"
raise exc
adapter._circuit_breaker.call = _always_fail
adapter._refresh_token_if_needed = AsyncMock(return_value=False)
result = await adapter._send_with_circuit(lambda: None)
assert result.success is False
assert "Token refresh failed" in result.error
class TestSendRateLimitRetry:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_send_retries_after_rate_limit_backoff(self, adapter):
call_count = 0
async def _fail_then_ok(*_args, **_kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
exc = Exception("rate limited")
exc.status_code = 429
exc.retry_after_ms = 10
raise exc
return DeliveryResult(success=True, message_id="$ok")
adapter._circuit_breaker.call = _fail_then_ok
result = await adapter._send_with_circuit(lambda: None)
assert result.success is True
assert call_count == 2
class TestMatrixAdapterCapabilities:
def test_chat_types_includes_thread(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert "thread" in adapter.capabilities.chat_types
def test_capabilities_includes_pin(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter.capabilities.pin is True
class TestAdapterReadMessages:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_read_messages_success(self, adapter):
mock_event = MagicMock()
mock_event.event_id = "$evt001"
mock_event.sender = "@user:example.org"
mock_event.origin_server_ts = 1700000000000
mock_event.source = {"content": {"body": "hello"}}
mock_resp = MagicMock()
mock_resp.chunk = [mock_event]
adapter._client.room_messages = AsyncMock(return_value=mock_resp)
messages = await adapter.read_messages("!room:example.org", limit=10)
assert len(messages) == 1
assert messages[0]["event_id"] == "$evt001"
assert messages[0]["sender"] == "@user:example.org"
@pytest.mark.asyncio
async def test_read_messages_no_client(self, adapter):
adapter._client = None
messages = await adapter.read_messages("!room:example.org")
assert messages == []
@pytest.mark.asyncio
async def test_read_messages_error_returns_empty(self, adapter):
adapter._client.room_messages = AsyncMock(side_effect=Exception("network error"))
messages = await adapter.read_messages("!room:example.org")
assert messages == []
class TestAdapterChannelInfo:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_get_channel_info_success(self, adapter):
name_resp = MagicMock()
name_resp.name = "Test Room"
topic_resp = MagicMock()
topic_resp.topic = "A test room"
join_rules = MagicMock()
join_rules.join_rule = "public"
members_resp = MagicMock()
members_resp.members = [MagicMock(), MagicMock()]
adapter._client.room_get_state_event = AsyncMock(side_effect=[name_resp, topic_resp, join_rules])
adapter._client.joined_members = AsyncMock(return_value=members_resp)
adapter._joined_rooms.add("!room:example.org")
info = await adapter.get_channel_info("!room:example.org")
assert info["name"] == "Test Room"
assert info["topic"] == "A test room"
assert info["join_rule"] == "public"
assert info["member_count"] == 2
assert info["is_joined"] is True
@pytest.mark.asyncio
async def test_get_channel_info_no_client(self, adapter):
adapter._client = None
info = await adapter.get_channel_info("!room:example.org")
assert info == {}
@pytest.mark.asyncio
async def test_get_channel_info_error_returns_partial(self, adapter):
adapter._client.room_get_state_event = AsyncMock(side_effect=Exception("error"))
info = await adapter.get_channel_info("!room:example.org")
assert "error" in info
class TestAdapterMemberInfo:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_get_member_info_success(self, adapter):
dn_resp = MagicMock()
dn_resp.displayname = "Alice"
avatar_resp = MagicMock()
avatar_resp.avatar_url = "mxc://example.org/avatar"
adapter._client.get_displayname = AsyncMock(return_value=dn_resp)
adapter._client.get_avatar = AsyncMock(return_value=avatar_resp)
info = await adapter.get_member_info("!room:example.org", "@alice:example.org")
assert info["display_name"] == "Alice"
assert info["avatar_url"] == "mxc://example.org/avatar"
@pytest.mark.asyncio
async def test_get_member_info_no_client(self, adapter):
adapter._client = None
info = await adapter.get_member_info("!room:example.org", "@alice:example.org")
assert info == {}
@pytest.mark.asyncio
async def test_get_member_info_error_returns_partial(self, adapter):
adapter._client.get_displayname = AsyncMock(side_effect=Exception("error"))
info = await adapter.get_member_info("!room:example.org", "@alice:example.org")
assert "error" in info
class TestAdapterSetProfile:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_set_profile_display_name(self, adapter):
adapter._client.set_displayname = AsyncMock()
result = await adapter.set_profile(display_name="MyBot")
assert result.success is True
adapter._client.set_displayname.assert_awaited_once_with("MyBot")
@pytest.mark.asyncio
async def test_set_profile_avatar(self, adapter):
adapter._client.set_avatar = AsyncMock()
result = await adapter.set_profile(avatar_url="mxc://example.org/avatar")
assert result.success is True
adapter._client.set_avatar.assert_awaited_once_with("mxc://example.org/avatar")
@pytest.mark.asyncio
async def test_set_profile_no_client(self, adapter):
adapter._client = None
result = await adapter.set_profile(display_name="MyBot")
assert result.success is False
assert result.error == "Client not initialized"
class TestAdapterPins:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_list_pins_success(self, adapter):
resp = MagicMock()
resp.pinned = ["$evt001", "$evt002"]
adapter._client.room_get_state_event = AsyncMock(return_value=resp)
pins = await adapter.list_pins("!room:example.org")
assert pins == ["$evt001", "$evt002"]
@pytest.mark.asyncio
async def test_list_pins_empty(self, adapter):
adapter._client.room_get_state_event = AsyncMock(return_value=None)
pins = await adapter.list_pins("!room:example.org")
assert pins == []
@pytest.mark.asyncio
async def test_list_pins_no_client(self, adapter):
adapter._client = None
pins = await adapter.list_pins("!room:example.org")
assert pins == []
@pytest.mark.asyncio
async def test_pin_message_success(self, adapter):
resp = MagicMock()
resp.pinned = ["$existing"]
adapter._client.room_get_state_event = AsyncMock(return_value=resp)
adapter._client.room_put_state = AsyncMock()
result = await adapter.pin_message("!room:example.org", "$new")
assert result.success is True
call_args = adapter._client.room_put_state.call_args
kwargs = call_args.kwargs if hasattr(call_args, "kwargs") else call_args[1] if len(call_args) > 1 else {}
@pytest.mark.asyncio
async def test_unpin_message_success(self, adapter):
resp = MagicMock()
resp.pinned = ["$evt001", "$evt002"]
adapter._client.room_get_state_event = AsyncMock(return_value=resp)
adapter._client.room_put_state = AsyncMock()
result = await adapter.unpin_message("!room:example.org", "$evt001")
assert result.success is True
@pytest.mark.asyncio
async def test_pin_message_no_client(self, adapter):
adapter._client = None
result = await adapter.pin_message("!room:example.org", "$evt")
assert result.success is False
class TestAdapterNewlineChunkMode:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org", "chunkMode": "newline"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_newline_chunk_mode_used(self, adapter):
from yuxi.channels.adapters.matrix.adapter import _split_content
content = "line1\nline2\nline3\nline4"
chunks = _split_content(content, limit=10, mode="newline")
assert len(chunks) >= 1
def test_split_by_newlines_basic(self):
from yuxi.channels.adapters.matrix.adapter import _split_by_newlines
text = "aaa\nbbb\nccc"
encoded = text.encode("utf-8")
chunks = _split_by_newlines(encoded, limit=10)
assert len(chunks) >= 1
def test_split_by_length_basic(self):
from yuxi.channels.adapters.matrix.adapter import _split_by_length
encoded = b"abcdefghij"
chunks = _split_by_length(encoded, limit=3)
assert len(chunks) == 4
def test_split_content_short_no_split(self):
from yuxi.channels.adapters.matrix.adapter import _split_content
chunks = _split_content("hello", limit=65536, mode="length")
assert chunks == ["hello"]
def test_split_content_newline_mode(self):
from yuxi.channels.adapters.matrix.adapter import _split_content
content = "A" * 100 + "\n" + "B" * 100
chunks = _split_content(content, limit=50, mode="newline")
assert len(chunks) >= 2
class TestAdapterResponsePrefix:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org", "responsePrefix": "[Bot]"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_send_adds_prefix(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
identity = ChannelIdentity(
channel_id="matrix",
channel_type=ChannelType.MATRIX,
channel_user_id="",
channel_chat_id="!room",
channel_message_id="$msg",
)
response = ChannelResponse(identity=identity, content="Hello")
await adapter.send(response)
call = adapter._circuit_breaker.call.await_args
assert call is not None
@pytest.mark.asyncio
async def test_send_no_prefix_when_empty_config(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
adapter.config["responsePrefix"] = ""
identity = ChannelIdentity(
channel_id="matrix",
channel_type=ChannelType.MATRIX,
channel_user_id="",
channel_chat_id="!room",
channel_message_id="$msg",
)
response = ChannelResponse(identity=identity, content="Hello")
await adapter.send(response)
assert True
class TestAdapterMediaSizeCheck:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org", "mediaMaxMb": 1})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_media_too_large_rejected(self, adapter):
large_data = b"x" * (2 * 1024 * 1024)
result = await adapter.send_media("!room:example.org", "file", large_data)
assert result.success is False
assert "exceeds" in result.error.lower()
@pytest.mark.asyncio
async def test_media_within_limit_accepted(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
small_data = b"x" * 1024
result = await adapter.send_media("!room:example.org", "file", small_data)
assert result.success is True
@pytest.mark.asyncio
async def test_media_size_check_uses_default_when_not_configured(self, adapter):
adapter.config = {"user_id": "@bot:example.org"}
adapter._circuit_breaker.call = AsyncMock(return_value=DeliveryResult(success=True, message_id="$evt"))
small_data = b"x" * 1024
result = await adapter.send_media("!room:example.org", "file", small_data)
assert result.success is True
class TestProbe:
@pytest.fixture
def mock_client(self):
from nio import AsyncClient
client = MagicMock(spec=AsyncClient)
client.homeserver = "https://example.org"
client.user_id = "@bot:example.org"
return client
@pytest.mark.asyncio
async def test_probe_matrix_success_with_whoami(self, mock_client):
from yuxi.channels.adapters.matrix.probe import probe_matrix
versions_resp = MagicMock()
versions_resp.versions = ["r0.6.1"]
whoami_resp = MagicMock()
whoami_resp.user_id = "@bot:example.org"
whoami_resp.device_id = "yuxi-bot"
whoami_resp.is_guest = False
mock_client.versions = AsyncMock(return_value=versions_resp)
mock_client.whoami = AsyncMock(return_value=whoami_resp)
result = await probe_matrix(mock_client, timeout_s=10)
assert result.status == "healthy"
assert "whoami" in result.metadata
assert result.metadata["whoami"]["user_id"] == "@bot:example.org"
@pytest.mark.asyncio
async def test_probe_matrix_whoami_error_graceful(self, mock_client):
from yuxi.channels.adapters.matrix.probe import probe_matrix
versions_resp = MagicMock()
versions_resp.versions = ["r0.6.1"]
mock_client.versions = AsyncMock(return_value=versions_resp)
mock_client.whoami = AsyncMock(side_effect=Exception("whoami failed"))
result = await probe_matrix(mock_client, timeout_s=10)
assert result.status == "healthy"
assert "error" in result.metadata["whoami"]
class TestSendContentChunking:
@pytest.fixture
def adapter(self):
a = MatrixAdapter({"user_id": "@bot:example.org"})
a._client = MagicMock()
a._client.user_id = "@bot:example.org"
return a
@pytest.mark.asyncio
async def test_short_text_not_chunked(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
identity = ChannelIdentity(
channel_id="matrix",
channel_type=ChannelType.MATRIX,
channel_user_id="",
channel_chat_id="!room",
channel_message_id="$msg",
)
response = ChannelResponse(identity=identity, content="short")
result = await adapter.send(response)
assert result.success is True
assert adapter._circuit_breaker.call.await_count == 1
@pytest.mark.asyncio
async def test_long_text_is_chunked(self, adapter):
mock_result = DeliveryResult(success=True, message_id="$evt")
adapter._circuit_breaker.call = AsyncMock(return_value=mock_result)
identity = ChannelIdentity(
channel_id="matrix",
channel_type=ChannelType.MATRIX,
channel_user_id="",
channel_chat_id="!room",
channel_message_id="$msg",
)
long_text = "A" * 70000
response = ChannelResponse(identity=identity, content=long_text)
result = await adapter.send(response)
assert result.success is True
assert adapter._circuit_breaker.call.await_count >= 2
@pytest.mark.asyncio
async def test_chunk_failure_stops_early(self, adapter):
call_count = 0
async def _mock_call(func):
nonlocal call_count
call_count += 1
return DeliveryResult(success=True if call_count == 0 else False, error="chunk failed")
adapter._circuit_breaker.call = _mock_call
identity = ChannelIdentity(
channel_id="matrix",
channel_type=ChannelType.MATRIX,
channel_user_id="",
channel_chat_id="!room",
channel_message_id="$msg",
)
long_text = "A" * 70000
response = ChannelResponse(identity=identity, content=long_text)
result = await adapter.send(response)
assert result.success is False
assert "chunk failed" in result.error
class TestReactionUtils:
def test_build_reaction(self):
from yuxi.channels.adapters.matrix.reaction_utils import build_reaction
result = build_reaction("👍", "$evt001")
relates_to = result["m.relates_to"]
assert relates_to["rel_type"] == "m.annotation"
assert relates_to["event_id"] == "$evt001"
assert relates_to["key"] == "👍"
def test_extract_reaction_valid(self):
from yuxi.channels.adapters.matrix.reaction_utils import extract_reaction
event = MagicMock()
event.source = {
"content": {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": "$evt001",
"key": "❤️",
},
},
}
result = extract_reaction(event)
assert result is not None
assert result["target_event_id"] == "$evt001"
assert result["key"] == "❤️"
def test_extract_reaction_not_annotation(self):
from yuxi.channels.adapters.matrix.reaction_utils import extract_reaction
event = MagicMock()
event.source = {
"content": {
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$evt002",
},
},
}
result = extract_reaction(event)
assert result is None
def test_summarize_reactions(self):
from yuxi.channels.adapters.matrix.reaction_utils import summarize_reactions
events = [
{"sender": "@a:example.org", "key": "👍", "target_event_id": "$evt001"},
{"sender": "@b:example.org", "key": "👍", "target_event_id": "$evt001"},
{"sender": "@a:example.org", "key": "❤️", "target_event_id": "$evt001"},
{"sender": "@c:example.org", "key": "👍", "target_event_id": "$evt002"},
]
result = summarize_reactions(events)
assert "$evt001" in result
assert result["$evt001"]["unique_reactors"] == 2
assert len(result["$evt001"]["keys"]) == 2
def test_select_reaction_exact_match(self):
from yuxi.channels.adapters.matrix.reaction_utils import select_reaction
available = [{"key": "👍"}, {"key": "❤️"}, {"key": "🎉"}]
result = select_reaction(available, "❤️")
assert result is not None
assert result["key"] == "❤️"
def test_select_reaction_fallback_to_first(self):
from yuxi.channels.adapters.matrix.reaction_utils import select_reaction
available = [{"key": "👍"}, {"key": "❤️"}]
result = select_reaction(available, "😊")
assert result is not None
assert result["key"] == "👍"
def test_select_reaction_empty_list(self):
from yuxi.channels.adapters.matrix.reaction_utils import select_reaction
result = select_reaction([], "👍")
assert result is None
class TestEncryption:
def test_resolve_encryption_config_path(self):
from yuxi.channels.adapters.matrix.encryption import resolve_encryption_config_path
path = resolve_encryption_config_path("/data", "https://example.org")
assert "matrix_crypto_store" in path
assert "example.org" in path
def test_format_encryption_unavailable_error(self):
from yuxi.channels.adapters.matrix.encryption import format_encryption_unavailable_error
msg = format_encryption_unavailable_error(
{"homeserver": "https://example.org", "crypto_store_dir": "/tmp"}
)
assert "E2EE is not configured" in msg
assert "/tmp" in msg
def test_check_crypto_store_ready_not_configured(self):
from yuxi.channels.adapters.matrix.encryption import check_crypto_store_ready
result = check_crypto_store_ready("")
assert result["status"] == "not_configured"
def test_check_crypto_store_ready_unavailable(self):
from yuxi.channels.adapters.matrix.encryption import check_crypto_store_ready
result = check_crypto_store_ready("/nonexistent/path/12345")
assert result["status"] == "unavailable"
def test_crypto_bootstrap_guide(self):
from yuxi.channels.adapters.matrix.encryption import get_crypto_bootstrap_guide
guide = get_crypto_bootstrap_guide()
assert "E2EE Bootstrap" in guide
class TestConcurrency:
@pytest.mark.asyncio
async def test_async_lock_acquire_release(self):
from yuxi.channels.adapters.matrix.concurrency import AsyncLock
lock = AsyncLock()
assert not lock.locked
async with lock:
assert lock.locked
assert not lock.locked
def test_startup_serial_lock_acquire(self):
from yuxi.channels.adapters.matrix.concurrency import StartupSerialLock
lock = StartupSerialLock()
assert lock.acquire_startup() is True
assert lock.is_starting is True
assert lock.acquire_startup() is False
def test_startup_serial_lock_release(self):
from yuxi.channels.adapters.matrix.concurrency import StartupSerialLock
lock = StartupSerialLock()
lock.acquire_startup()
lock.release_startup()
assert lock.is_starting is False
@pytest.mark.asyncio
async def test_account_data_write_queue_enqueue_drain(self):
from yuxi.channels.adapters.matrix.concurrency import AccountDataWriteQueue
queue = AccountDataWriteQueue()
await queue.enqueue("m.direct", {"@user:example.org": ["!room:example.org"]})
items = await queue.drain()
assert len(items) == 1
assert items[0][0] == "m.direct"
class TestConfigPatch:
def test_matrix_account_patch_from_dict(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch
patch = MatrixAccountPatch.from_dict({"display_name": "NewBot", "sync_timeout_ms": 60000})
assert patch.display_name == "NewBot"
assert patch.sync_timeout_ms == 60000
assert patch.has_changes is True
def test_matrix_account_patch_empty(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch
patch = MatrixAccountPatch()
assert patch.has_changes is False
def test_apply_patch_updates_config(self):
from yuxi.channels.adapters.matrix.config_patch import apply_patch, MatrixAccountPatch
config = {"user_id": "@bot:example.org", "display_name": "OldBot"}
patch = MatrixAccountPatch.from_dict({"display_name": "NewBot", "sync_timeout_ms": 60000})
result = apply_patch(config, patch)
assert result["display_name"] == "NewBot"
assert result["sync_timeout_ms"] == 60000
assert result["user_id"] == "@bot:example.org"
def test_merge_patches(self):
from yuxi.channels.adapters.matrix.config_patch import merge_patches, MatrixAccountPatch
base = MatrixAccountPatch(display_name="A", sync_timeout_ms=30000)
overlay = MatrixAccountPatch(display_name="B")
merged = merge_patches(base, overlay)
assert merged.display_name == "B"
assert merged.sync_timeout_ms == 30000
def test_diff_config(self):
from yuxi.channels.adapters.matrix.config_patch import diff_config
old = {"a": 1, "b": 2}
new = {"a": 1, "b": 3, "c": 4}
diff = diff_config(old, new)
assert "b" in diff
assert "c" in diff
assert "a" not in diff
def test_to_dict_extra_fields(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch
patch = MatrixAccountPatch.from_dict({"custom_key": "custom_value"})
assert patch.extra == {"custom_key": "custom_value"}
d = patch.to_dict()
assert "custom_key" in d
class TestGroupMentions:
def test_require_mention_from_room_config(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
config = {
"requireMention": True,
"rooms": {"!room1:example.org": {"requireMention": False}},
}
strategy = GroupMentionStrategy(config)
assert strategy.should_require_mention("!room1:example.org") is False
assert strategy.should_require_mention("!room2:example.org") is True
def test_is_at_mentioned(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
config = {"user_id": "@bot:example.org"}
strategy = GroupMentionStrategy(config)
result = strategy.is_at_mentioned(
"@bot:example.org",
"!room:example.org",
"hello @bot:example.org",
'<a href="https://matrix.to/#/@bot:example.org">bot</a>',
)
assert result is True
def test_evaluate_room_policy_no_mention_required(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
config = {"requireMention": False}
strategy = GroupMentionStrategy(config)
result = strategy.evaluate_room_policy("!room:example.org", "@user:example.org", "hello", "")
assert result is True
def test_build_room_mention_config(self):
from yuxi.channels.adapters.matrix.group_mentions import build_room_mention_config
result = build_room_mention_config(
"!room:example.org",
require_mention=True,
allowed_roles=["admin"],
mention_exempt_users=["@admin:example.org"],
)
assert "!room:example.org" in result
assert result["!room:example.org"]["requireMention"] is True
assert "admin" in result["!room:example.org"]["allowedRoles"]
class TestProfileSync:
def test_resolve_avatar_url_mxc_passthrough(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_avatar_url
result = resolve_avatar_url("mxc://example.org/avatar001")
assert result == "mxc://example.org/avatar001"
def test_resolve_avatar_url_http_to_mxc(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_avatar_url
result = resolve_avatar_url("https://cdn.example.org/avatar.png", "example.org")
assert result is not None
assert result.startswith("mxc://")
def test_resolve_avatar_url_empty(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_avatar_url
assert resolve_avatar_url("") is None
assert resolve_avatar_url("", "example.org") is None
def test_resolve_display_name(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_display_name
assert resolve_display_name({"displayname": "Alice"}) == "Alice"
assert resolve_display_name({"user_id": "@alice:example.org"}) == "alice"
assert resolve_display_name({}, "@fallback:example.org") == "fallback"
def test_compare_profiles_detects_changes(self):
from yuxi.channels.adapters.matrix.profile_sync import compare_profiles
current = {"displayname": "Old", "avatar_url": "mxc://old"}
incoming = {"displayname": "New", "avatar_url": "mxc://new"}
changes = compare_profiles(current, incoming)
assert len(changes) == 2
assert changes["displayname"] == ("Old", "New")
def test_mxc_to_http_conversion(self):
from yuxi.channels.adapters.matrix.profile_sync import mxc_to_http
url = mxc_to_http("mxc://example.org/abc123", "https://matrix.example.org")
assert url.startswith("https://")
assert "abc123" in url
assert "example.org" in url
class TestSessionTargetID:
@pytest.fixture
def adapter(self):
return MatrixAdapter({"user_id": "@bot:example.org"})
def test_parse_target_id_matrix_uri(self, adapter):
result = adapter._session.parse_target_id("matrix:!room:example.org")
assert result["type"] == "matrix_uri"
assert result["value"] == "!room:example.org"
def test_parse_target_id_room_prefix(self, adapter):
result = adapter._session.parse_target_id("room:!room:example.org")
assert result["type"] == "room_id"
def test_parse_target_id_user_prefix(self, adapter):
result = adapter._session.parse_target_id("user:@alice:example.org")
assert result["type"] == "user_id"
def test_parse_target_id_mxid(self, adapter):
result = adapter._session.parse_target_id("@alice:example.org")
assert result["type"] == "user_id"
def test_parse_target_id_room_id(self, adapter):
result = adapter._session.parse_target_id("!room:example.org")
assert result["type"] == "room_id"
def test_parse_target_id_room_alias(self, adapter):
result = adapter._session.parse_target_id("#general:example.org")
assert result["type"] == "room_alias"
def test_parse_target_id_unknown(self, adapter):
result = adapter._session.parse_target_id("random_string")
assert result["type"] == "unknown"
def test_resolve_target_to_room_room_id(self, adapter):
result = adapter._session.resolve_target_to_room("!room:example.org")
assert result == "!room:example.org"
def test_resolve_session_metadata(self, adapter):
event_source = {
"content": {
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$root",
},
"info": {"mimetype": "image/png", "size": 1024},
},
}
metadata = adapter._session.resolve_session_metadata("!room:example.org", event_source)
assert metadata["is_thread"] is True
assert metadata["thread_root"] == "$root"
assert metadata["mimetype"] == "image/png"
def test_resolve_participant_metadata(self, adapter):
adapter._client = MagicMock()
adapter._client.user_id = "@bot:example.org"
metadata = adapter._session.resolve_participant_metadata(
"@bot:example.org", "MyBot", "mxc://avatar"
)
assert metadata["is_bot_self"] is True
assert metadata["display_name"] == "MyBot"
class TestProbeDeviceBackup:
def test_describe_backup_state_known(self):
from yuxi.channels.adapters.matrix.probe import describe_backup_state
assert describe_backup_state(1) == "keys_backed_up_and_verified"
assert describe_backup_state(5) == "backup_disabled"
assert describe_backup_state(99) == "unknown_state_99"
def test_is_backup_healthy(self):
from yuxi.channels.adapters.matrix.probe import is_backup_healthy
assert is_backup_healthy(1) is True
assert is_backup_healthy(2) is True
assert is_backup_healthy(0) is False
assert is_backup_healthy(4) is False
def test_check_device_health_known_prefix(self):
from yuxi.channels.adapters.matrix.probe import _check_device_health
result = _check_device_health({"device_id": "yuxi-bot-abc123"})
assert result["status"] == "healthy"
assert result["is_known_prefix"] == "true"
def test_check_device_health_unrecognized(self):
from yuxi.channels.adapters.matrix.probe import _check_device_health
result = _check_device_health({"device_id": "random-device"})
assert result["status"] == "unrecognized"
def test_check_device_health_no_device_id(self):
from yuxi.channels.adapters.matrix.probe import _check_device_health
result = _check_device_health({})
assert result["status"] == "unknown"
class TestAdapterEnhancedStreamingModes:
def test_streaming_modes_include_lane_and_reasoning(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert "lane" in adapter.streaming_modes
assert "reasoning" in adapter.streaming_modes
assert "off" in adapter.streaming_modes
assert "block" in adapter.streaming_modes
def test_capabilities_include_lane_streaming(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter.capabilities.lane_streaming is True
assert adapter.capabilities.reasoning_streaming is True
assert adapter.capabilities.block_streaming is True
def test_mention_strategy_initialized(self):
adapter = MatrixAdapter({"user_id": "@bot:example.org"})
assert adapter._mention_strategy is not None
class TestPolls:
def test_extract_poll_start_valid(self):
from yuxi.channels.adapters.matrix.polls import extract_poll_start
event_source = {
"content": {
"org.matrix.msc3381.poll.start": {
"question": {"body": "What?", "kind": "open", "msgtype": "m.text"},
"answers": [
{"id": "answer_0", "org.matrix.msc1767.text": "A"},
{"id": "answer_1", "org.matrix.msc1767.text": "B"},
],
"max_selections": 1,
},
},
}
result = extract_poll_start(event_source)
assert result is not None
assert result["question"] == "What?"
assert len(result["answers"]) == 2
assert result["answers"][0]["text"] == "A"
def test_extract_poll_start_missing(self):
from yuxi.channels.adapters.matrix.polls import extract_poll_start
assert extract_poll_start({"content": {}}) is None
def test_extract_poll_response_valid(self):
from yuxi.channels.adapters.matrix.polls import extract_poll_response
event_source = {
"content": {
"m.relates_to": {"rel_type": "m.reference", "event_id": "$poll"},
"org.matrix.msc3381.poll.response": {"answers": ["answer_0"]},
},
}
result = extract_poll_response(event_source)
assert result is not None
assert result["event_id"] == "$poll"
assert result["answers"] == ["answer_0"]
def test_extract_poll_response_missing(self):
from yuxi.channels.adapters.matrix.polls import extract_poll_response
assert extract_poll_response({"content": {}}) is None
def test_is_poll_end_true(self):
from yuxi.channels.adapters.matrix.polls import is_poll_end
assert is_poll_end({"content": {"org.matrix.msc3381.poll.end": {}}}) is True
def test_is_poll_end_false(self):
from yuxi.channels.adapters.matrix.polls import is_poll_end
assert is_poll_end({"content": {}}) is False
def test_summarize_poll_responses(self):
from yuxi.channels.adapters.matrix.polls import summarize_poll_responses
responses = [
{"event_id": "$poll", "poll_response": {"answers": ["answer_0"]}},
{"event_id": "$poll", "poll_response": {"answers": ["answer_0"]}},
{"event_id": "$poll", "poll_response": {"answers": ["answer_1"]}},
]
tally = summarize_poll_responses(responses)
assert "$poll" in tally
assert tally["$poll"]["answer_0"] == 2
assert tally["$poll"]["answer_1"] == 1
class TestLocation:
def test_parse_location_event_with_msgtype(self):
from yuxi.channels.adapters.matrix.location import parse_location_event
event_source = {
"content": {
"msgtype": "m.location",
"body": "Meet us here",
"geo_uri": "geo:51.5007,-0.1246",
},
}
result = parse_location_event(event_source)
assert result is not None
assert result["type"] == "location"
assert "Meet us here" in result["text"]
assert "geo:51.5007,-0.1246" in result["geo_uri"]
def test_parse_location_event_geo_uri_only(self):
from yuxi.channels.adapters.matrix.location import parse_location_event
assert parse_location_event({"content": {"geo_uri": "geo:40.0,-74.0"}}) is not None
def test_parse_location_event_no_location(self):
from yuxi.channels.adapters.matrix.location import parse_location_event
assert parse_location_event({"content": {"body": "hello"}}) is None
def test_extract_geo_coordinates_valid(self):
from yuxi.channels.adapters.matrix.location import extract_geo_coordinates
result = extract_geo_coordinates("geo:51.5007,-0.1246")
assert result is not None
assert result["lat"] == 51.5007
assert result["lon"] == -0.1246
def test_extract_geo_coordinates_invalid(self):
from yuxi.channels.adapters.matrix.location import extract_geo_coordinates
assert extract_geo_coordinates("invalid") is None
assert extract_geo_coordinates("notgeo:") is None
def test_build_outbound_location(self):
from yuxi.channels.adapters.matrix.location import build_outbound_location
result = build_outbound_location("Office", 37.7749, -122.4194, desc="SF Office")
assert result["msgtype"] == "m.location"
assert result["geo_uri"] == "geo:37.7749,-122.4194"
def test_format_location_text_with_body(self):
from yuxi.channels.adapters.matrix.location import format_location_text
text = format_location_text("Park", "geo:1,2")
assert "[位置]" in text
class TestSticker:
def test_is_sticker_event_true(self):
from yuxi.channels.adapters.matrix.sticker import is_sticker_event
assert is_sticker_event({"content": {"msgtype": "m.sticker"}}) is True
def test_is_sticker_event_false(self):
from yuxi.channels.adapters.matrix.sticker import is_sticker_event
assert is_sticker_event({"content": {"msgtype": "m.text"}}) is False
def test_parse_sticker_valid(self):
from yuxi.channels.adapters.matrix.sticker import parse_sticker
event_source = {
"content": {
"msgtype": "m.sticker",
"body": "cute",
"url": "mxc://example.org/sticker1",
"info": {"mimetype": "image/png", "w": 128, "h": 128},
},
}
result = parse_sticker(event_source)
assert result is not None
assert result["type"] == "sticker"
assert result["url"] == "mxc://example.org/sticker1"
assert result["width"] == 128
def test_build_outbound_sticker(self):
from yuxi.channels.adapters.matrix.sticker import build_outbound_sticker
result = build_outbound_sticker("mxc://example.org/st1", body="hello", width=64, height=64)
assert result["msgtype"] == "m.sticker"
assert result["url"] == "mxc://example.org/st1"
assert result["info"]["w"] == 64
class TestSSRFGuard:
def test_is_private_host_localhost(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("localhost") is True
assert SSRFGuard.is_private_host("127.0.0.1") is True
def test_is_private_host_private_range(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("192.168.1.1") is True
assert SSRFGuard.is_private_host("10.0.0.1") is True
def test_is_private_host_public(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("93.184.216.34") is False
assert SSRFGuard.is_private_host("matrix.org") is False
def test_is_private_host_empty(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("") is False
def test_validate_url_blocks_private(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
error = SSRFGuard.validate_url("http://127.0.0.1:8008")
assert error is not None
assert "private" in error.lower()
def test_validate_url_allows_private_when_enabled(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
error = SSRFGuard.validate_url("http://192.168.1.1", allow_private=True)
assert error is None
def test_validate_url_public_passes(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
error = SSRFGuard.validate_url("https://matrix.org")
assert error is None
def test_validate_url_empty_returns_error(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.validate_url("") is not None
class TestSyncTokenStore:
def test_save_and_load_token(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_store import SyncTokenStore
store = SyncTokenStore(str(tmp_path), "@user:example.org")
store.save("s12345")
assert store.load() == "s12345"
def test_load_nonexistent_returns_none(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_store import SyncTokenStore
store = SyncTokenStore(str(tmp_path / "nonexistent"), "@user:example.org")
assert store.load() is None
def test_clear_removes_token(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_store import SyncTokenStore
store = SyncTokenStore(str(tmp_path), "@user:example.org")
store.save("s12345")
store.clear()
assert store.load() is None
class TestMultiAccountManager:
def test_load_accounts(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {
"accounts": {
"bot1": {"user_id": "@bot1:example.org", "homeserver": "https://example1.org", "isDefault": True},
"bot2": {"user_id": "@bot2:example.org", "homeserver": "https://example2.org"},
},
}
mgr = MultiAccountManager(config)
assert mgr.account_count == 2
assert mgr.default_account.user_id == "@bot1:example.org"
def test_list_accounts(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {
"accounts": {"bot1": {"user_id": "@bot1:example.org"}},
}
mgr = MultiAccountManager(config)
accounts = mgr.list_accounts()
assert len(accounts) == 1
assert accounts[0].user_id == "@bot1:example.org"
def test_get_account(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {
"accounts": {
"bot1": {"user_id": "@bot1:example.org"},
"bot2": {"user_id": "@bot2:example.org"},
},
"defaultAccount": "bot2",
}
mgr = MultiAccountManager(config)
acc = mgr.get("bot2")
assert acc.user_id == "@bot2:example.org"
def test_select_account(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {
"accounts": {
"bot1": {"user_id": "@bot1:example.org"},
"bot2": {"user_id": "@bot2:example.org"},
},
}
mgr = MultiAccountManager(config)
assert mgr.select_account("bot2") is True
assert mgr.default_account_id == "bot2"
def test_select_nonexistent_account(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {"accounts": {"bot1": {"user_id": "@bot1:example.org"}}}
mgr = MultiAccountManager(config)
assert mgr.select_account("nonexistent") is False
def test_account_to_dict(self):
from yuxi.channels.adapters.matrix.accounts import MatrixAccount
acc = MatrixAccount("bot1", {"user_id": "@bot1:example.org", "homeserver": "https://example.org"})
d = acc.to_dict()
assert d["user_id"] == "@bot1:example.org"
assert d["account_id"] == "bot1"
class TestThreadBindingManager:
def test_bind_creates_entry(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({})
binding = mgr.bind("!room:example.org", "$root123", session_key="sess1")
assert binding.room_id == "!room:example.org"
assert binding.session_key == "sess1"
assert mgr.binding_count == 1
def test_bind_same_thread_returns_existing(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({})
b1 = mgr.bind("!room:example.org", "$root123")
b2 = mgr.bind("!room:example.org", "$root123")
assert b1 is b2
def test_unbind_removes_entry(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({})
mgr.bind("!room:example.org", "$root123")
mgr.unbind("!room:example.org", "$root123")
assert mgr.binding_count == 0
def test_get_nonexistent_returns_none(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({})
assert mgr.get("!room:example.org", "$nonexistent") is None
def test_idle_timeout_expiry(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({"threadBindings": {"idleHours": 0}})
mgr.bind("!room:example.org", "$root123")
expired = mgr.check_timeouts()
assert len(expired) == 1
class TestDirectRoom:
def test_promote_room_to_direct_returns_status(self):
mock_client = MagicMock()
mock_adata = MagicMock()
mock_adata.content = {}
mock_client.get_account_data = AsyncMock(return_value=mock_adata)
mock_client.set_account_data = AsyncMock()
adapter = MatrixAdapter({})
adapter._client = mock_client
import asyncio
result = asyncio.get_event_loop().run_until_complete(
adapter.promote_room_to_direct("!room:example.org", "@alice:example.org")
)
assert result["status"] == "promoted"
def test_demote_direct_room(self):
mock_client = MagicMock()
mock_adata = MagicMock()
mock_adata.content = {"@alice:example.org": ["!room:example.org"]}
mock_client.get_account_data = AsyncMock(return_value=mock_adata)
mock_client.set_account_data = AsyncMock()
adapter = MatrixAdapter({})
adapter._client = mock_client
import asyncio
result = asyncio.get_event_loop().run_until_complete(
adapter.demote_direct_room("!room:example.org", "@alice:example.org")
)
assert result["status"] == "demoted"
class TestEnvVars:
def test_resolve_env_vars_populates_homeserver(self):
import os
from yuxi.channels.adapters.matrix.env_vars import resolve_env_vars
os.environ["MATRIX_HOMESERVER"] = "https://custom.example.org"
result = resolve_env_vars({"user_id": "@bot:example.org"})
assert result["homeserver"] == "https://custom.example.org"
del os.environ["MATRIX_HOMESERVER"]
def test_resolve_env_vars_does_not_override_existing(self):
import os
from yuxi.channels.adapters.matrix.env_vars import resolve_env_vars
result = resolve_env_vars({"homeserver": "https://explicit.example.org"})
assert result["homeserver"] == "https://explicit.example.org"
def test_get_env_var_descriptions(self):
from yuxi.channels.adapters.matrix.env_vars import get_env_var_descriptions
descs = get_env_var_descriptions()
assert "MATRIX_HOMESERVER" in descs
assert "MATRIX_ACCESS_TOKEN" in descs
def test_coerce_bool_values(self):
from yuxi.channels.adapters.matrix.env_vars import _coerce_type
assert _coerce_type("true", True) is True
assert _coerce_type("false", True) is False
assert _coerce_type("1", True) is True
assert _coerce_type("yes", True) is True
def test_coerce_int_values(self):
from yuxi.channels.adapters.matrix.env_vars import _coerce_type
assert _coerce_type("42", 0) == 42
assert _coerce_type("invalid", 0) == "invalid"
class TestConfigSchema:
def test_validate_config_required_user_id(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({})
assert any(e["field"] == "user_id" for e in errors)
def test_validate_config_missing_auth(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({"user_id": "@bot:example.org"})
assert any("auth" in e["field"] for e in errors)
def test_validate_config_valid(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"homeserver": "https://example.org",
"access_token": "token123",
})
assert len(errors) == 0
def test_get_default_config(self):
from yuxi.channels.adapters.matrix.config_schema import get_default_config
defaults = get_default_config()
assert defaults["homeserver"] == "https://matrix.org"
assert defaults["device_id"] == "yuxi-bot"
def test_get_config_schema(self):
from yuxi.channels.adapters.matrix.config_schema import get_config_schema
schema = get_config_schema()
assert "homeserver" in schema
assert "user_id" in schema
assert schema["user_id"]["required"] is True
class TestCredentials:
def test_matrix_credentials_to_dict(self):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials
creds = MatrixCredentials(
user_id="@bot:example.org",
homeserver="https://example.org",
access_token="tok123",
device_id="dev001",
)
d = creds.to_dict()
assert d["user_id"] == "@bot:example.org"
assert d["access_token"] == "tok123"
def test_matrix_credentials_is_valid(self):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials
valid = MatrixCredentials(user_id="@b:a", homeserver="https://a", access_token="t")
assert valid.is_valid is True
invalid = MatrixCredentials()
assert invalid.is_valid is False
def test_credential_store_save_and_load(self, tmp_path):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials, CredentialStore
store = CredentialStore(base_dir=str(tmp_path))
creds = MatrixCredentials(user_id="@bot:a", homeserver="https://a", access_token="tok")
store.save(creds)
loaded = store.load("@bot:a")
assert loaded is not None
assert loaded.user_id == "@bot:a"
def test_credential_store_load_nonexistent(self, tmp_path):
from yuxi.channels.adapters.matrix.credentials import CredentialStore
store = CredentialStore(base_dir=str(tmp_path))
assert store.load("nonexistent") is None
def test_credential_store_delete(self, tmp_path):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials, CredentialStore
store = CredentialStore(base_dir=str(tmp_path))
creds = MatrixCredentials(user_id="@bot:a", homeserver="https://a", access_token="tok")
store.save(creds)
store.delete("@bot:a")
assert store.load("@bot:a") is None
class TestSyncStateMachine:
def test_initial_state_is_prepared(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
assert sm.state == SyncState.PREPARED
def test_transition_to_syncing(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
assert sm.to_healthy() is True
assert sm.state == SyncState.SYNCING
assert sm.is_healthy is True
def test_transition_to_error(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
sm.to_healthy()
assert sm.to_error("connection lost") is True
assert sm.state == SyncState.ERROR
assert sm.last_error == "connection lost"
assert sm.is_healthy is False
def test_transition_to_reconnecting(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
sm.to_healthy()
sm.to_error()
assert sm.to_reconnecting() is True
assert sm.state == SyncState.RECONNECTING
def test_retry_count_increments(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine
sm = SyncStateMachine()
sm.to_healthy()
for _ in range(5):
sm.to_error()
sm.to_reconnecting()
sm.to_healthy()
assert sm.retry_count == 0
class TestSentMessageCache:
def test_add_and_get(self):
from yuxi.channels.adapters.matrix.sent_cache import SentMessageCache
cache = SentMessageCache()
cache.add("!room:example.org", "hash1", "$evt001")
assert cache.get("!room:example.org", "hash1") == "$evt001"
def test_get_content_hash_match(self):
from yuxi.channels.adapters.matrix.sent_cache import SentMessageCache, hash_content
cache = SentMessageCache()
content_hash = hash_content("hello world")
cache.add("!room:example.org", content_hash, "$evt002")
event_id = cache.get_event_id_for_content("!room:example.org", "hello world")
assert event_id == "$evt002"
def test_remove_by_event_id(self):
from yuxi.channels.adapters.matrix.sent_cache import SentMessageCache
cache = SentMessageCache()
cache.add("!room:example.org", "hash1", "$evt001")
cache.remove("!room:example.org", "$evt001")
assert cache.get("!room:example.org", "hash1") is None
def test_clear_room(self):
from yuxi.channels.adapters.matrix.sent_cache import SentMessageCache
cache = SentMessageCache()
cache.add("!room1:example.org", "hash1", "$evt001")
cache.add("!room2:example.org", "hash2", "$evt002")
cache.clear_room("!room1:example.org")
assert cache.get("!room1:example.org", "hash1") is None
assert cache.get("!room2:example.org", "hash2") == "$evt002"
class TestResponseSizeLimiter:
def test_check_within_limit(self):
from yuxi.channels.adapters.matrix.sent_cache import ResponseSizeLimiter
limiter = ResponseSizeLimiter(max_bytes=1000)
ok, _ = limiter.check(500)
assert ok is True
def test_check_exceeds_limit(self):
from yuxi.channels.adapters.matrix.sent_cache import ResponseSizeLimiter
limiter = ResponseSizeLimiter(max_bytes=1000)
ok, msg = limiter.check(2000)
assert ok is False
assert "exceeds" in msg
class TestVoice:
def test_is_voice_message_with_msc3245(self):
from yuxi.channels.adapters.matrix.voice import is_voice_message
event_source = {
"content": {"msgtype": "m.audio", "org.matrix.msc3245.voice": {}},
}
assert is_voice_message(event_source) is True
def test_is_voice_message_with_msc1767(self):
from yuxi.channels.adapters.matrix.voice import is_voice_message
event_source = {
"content": {"msgtype": "m.audio", "org.matrix.msc1767.audio": {"duration": 5000}},
}
assert is_voice_message(event_source) is True
def test_is_voice_message_not_audio(self):
from yuxi.channels.adapters.matrix.voice import is_voice_message
event_source = {"content": {"msgtype": "m.text"}}
assert is_voice_message(event_source) is False
def test_extract_audio_info(self):
import time
from yuxi.channels.adapters.matrix.voice import extract_audio_info
event_source = {
"content": {
"msgtype": "m.audio",
"info": {"duration": 15000, "mimetype": "audio/ogg", "size": 32768},
"org.matrix.msc3245.voice": {},
},
}
info = extract_audio_info(event_source)
assert info["duration_ms"] == 15000
assert info["mime_type"] == "audio/ogg"
assert info["is_voice"] is True
class TestMediaMeta:
def test_extract_duration_bytes_ogg(self):
from yuxi.channels.adapters.matrix.media_meta import extract_duration_bytes
duration = extract_duration_bytes(b"OggS\x00\x02" + b"\x00" * 1000, "audio/ogg")
assert duration >= 0
def test_extract_duration_bytes_mp3(self):
from yuxi.channels.adapters.matrix.media_meta import extract_duration_bytes
duration = extract_duration_bytes(b"\xff\xfb\x90" + b"\x00" * 1000, "audio/mp3")
assert duration >= 0
class TestSetupWizard:
def test_build_steps_returns_list(self):
from yuxi.channels.adapters.matrix.setup_wizard import MatrixSetupWizard
wizard = MatrixSetupWizard()
steps = wizard.build_steps()
assert len(steps) >= 5
assert steps[0]["id"] == "homeserver"
def test_advance_advances_step(self):
from yuxi.channels.adapters.matrix.setup_wizard import MatrixSetupWizard
wizard = MatrixSetupWizard()
assert wizard.advance() is True
def test_go_back_returns_false_at_start(self):
from yuxi.channels.adapters.matrix.setup_wizard import MatrixSetupWizard
wizard = MatrixSetupWizard()
assert wizard.go_back() is False
def test_to_config_produces_valid_config(self):
from yuxi.channels.adapters.matrix.setup_wizard import MatrixSetupWizard
wizard = MatrixSetupWizard()
wizard.set_answer("homeserver", "https://example.org")
wizard.set_answer("user_id", "@bot:example.org")
wizard.set_answer("auth_method", "password")
wizard.set_answer("credential", "secret")
config = wizard.to_config()
assert config["homeserver"] == "https://example.org"
assert config["user_id"] == "@bot:example.org"
assert config["password"] == "secret"
class TestCommands:
def test_detect_slash_command_help(self):
from yuxi.channels.adapters.matrix.commands import detect_slash_command
result = detect_slash_command("/help")
assert result is not None
assert result["command"] == "/help"
assert result["handler"] == "show_help"
def test_detect_slash_command_unknown(self):
from yuxi.channels.adapters.matrix.commands import detect_slash_command
assert detect_slash_command("/unknown") is None
def test_detect_slash_command_not_command(self):
from yuxi.channels.adapters.matrix.commands import detect_slash_command
assert detect_slash_command("hello world") is None
def test_is_slash_command_true(self):
from yuxi.channels.adapters.matrix.commands import is_slash_command
assert is_slash_command("/help") is True
def test_is_slash_command_false(self):
from yuxi.channels.adapters.matrix.commands import is_slash_command
assert is_slash_command("help") is False
def test_format_help_text(self):
from yuxi.channels.adapters.matrix.commands import format_help_text
text = format_help_text()
assert "/help" in text
assert "/new" in text
def test_get_command_list(self):
from yuxi.channels.adapters.matrix.commands import get_command_list
cmds = get_command_list()
assert any(c["command"] == "/help" for c in cmds)
def test_detect_slash_command_with_args(self):
from yuxi.channels.adapters.matrix.commands import detect_slash_command
result = detect_slash_command("/proxy spawn bind_to_thread")
assert result is not None
assert result["command"] == "/proxy"
assert result["args"].get("spawn") == "spawn"
class TestSubagentHooks:
def test_build_subagent_event(self):
from yuxi.channels.adapters.matrix.subagent_hooks import build_subagent_event
event = build_subagent_event("spawning", "!room:example.org", "$root", subagent_id="sub1")
assert event["phase"] == "spawning"
assert event["subagent_id"] == "sub1"
def test_subagent_lifecycle_on_and_trigger(self):
from yuxi.channels.adapters.matrix.subagent_hooks import SubagentLifecycle
results = []
async def cb(event):
results.append(event["phase"])
lifecycle = SubagentLifecycle()
lifecycle.on("spawning", cb)
import asyncio
asyncio.get_event_loop().run_until_complete(
lifecycle.trigger("spawning", "!room:example.org", "$root")
)
assert len(results) == 1
assert results[0] == "spawning"
class TestExecApprovals:
def test_is_approver_no_list(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({"execApprovals": {"enabled": "true"}})
assert mgr.is_approver("@anyone:example.org") is True
def test_is_approver_with_list(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({
"execApprovals": {"enabled": "true", "approvers": ["@admin:example.org"]},
})
assert mgr.is_approver("@admin:example.org") is True
assert mgr.is_approver("@user:example.org") is False
def test_requires_approval_when_enabled(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({"execApprovals": {"enabled": "true"}})
assert mgr.requires_approval("!room:example.org", "rm -rf /") is True
def test_requires_approval_when_disabled(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({"execApprovals": {"enabled": "false"}})
assert mgr.requires_approval("!room:example.org", "ls") is False
def test_create_request_and_handle_approval(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({"execApprovals": {"enabled": "true"}})
request = mgr.create_request("req1", "dangerous cmd", "!room:example.org", "@user:example.org")
assert request.status == "pending"
result = mgr.handle_reaction("@admin:example.org", "\u2705", "req1")
assert result == "allow-once"
assert request.status == "approved"
def test_handle_reaction_deny(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({"execApprovals": {"enabled": "true"}})
mgr.create_request("req1", "cmd", "!room:example.org", "@user:example.org")
result = mgr.handle_reaction("@admin:example.org", "\u274c", "req1")
assert result == "deny"
def test_get_pending(self):
from yuxi.channels.adapters.matrix.exec_approvals import ExecApprovalManager
mgr = ExecApprovalManager({"execApprovals": {"enabled": "true"}})
mgr.create_request("req1", "cmd1", "!room:example.org", "@user:example.org")
mgr.create_request("req2", "cmd2", "!room:example.org", "@user:example.org")
assert len(mgr.get_pending()) == 2
class TestACPBindings:
def test_create_binding(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBindingManager
mgr = ACPBindingManager({})
binding = mgr.create_binding("!room:example.org", "$root", session_id="acp1")
assert binding.acp_session_id == "acp1"
assert not binding.bound
def test_get_nonexistent_returns_none(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBindingManager
mgr = ACPBindingManager({})
assert mgr.get("!room:example.org", "$nonexistent") is None
def test_remove_binding(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBindingManager
mgr = ACPBindingManager({})
mgr.create_binding("!room:example.org", "$root")
assert mgr.remove("!room:example.org", "$root") is True
assert mgr.get("!room:example.org", "$root") is None
def test_list_bindings(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBindingManager
mgr = ACPBindingManager({})
mgr.create_binding("!room1:example.org", "$root1")
mgr.create_binding("!room2:example.org", "$root2")
assert len(mgr.list_bindings()) == 2
class TestDraftStream:
def test_remove_draft(self):
from yuxi.channels.adapters.matrix.draft_stream import (
get_or_create_draft,
remove_draft,
)
draft = get_or_create_draft("!room:example.org")
assert draft.room_id == "!room:example.org"
assert not draft.is_active
remove_draft("!room:example.org")
draft2 = get_or_create_draft("!room:example.org")
assert draft is not draft2
class TestConfigUIHints:
def test_get_config_hints_returns_dict(self):
from yuxi.channels.adapters.matrix.config_ui_hints import get_config_hints
hints = get_config_hints()
assert len(hints) > 10
assert "homeserver" in hints
assert hints["homeserver"]["group"] == "connection"
def test_get_hint_specific_key(self):
from yuxi.channels.adapters.matrix.config_ui_hints import get_hint
hint = get_hint("homeserver")
assert hint is not None
assert hint["label"] == "Homeserver URL"
def test_get_hint_nonexistent(self):
from yuxi.channels.adapters.matrix.config_ui_hints import get_hint
assert get_hint("nonexistent") is None
def test_get_group_hints(self):
from yuxi.channels.adapters.matrix.config_ui_hints import get_group_hints
hints = get_group_hints("security")
assert len(hints) > 0
assert all(v["group"] == "security" for v in hints.values())
def test_get_groups(self):
from yuxi.channels.adapters.matrix.config_ui_hints import get_groups
groups = get_groups()
assert "connection" in groups
assert "security" in groups
assert groups["connection"] == "连接设置"
class TestMatrixCLI:
def test_dispatch_no_args(self):
from yuxi.channels.adapters.matrix.cli import MatrixCLI
cli = MatrixCLI()
result = cli.dispatch([])
assert result["status"] == "error"
def test_dispatch_unknown_command(self):
from yuxi.channels.adapters.matrix.cli import MatrixCLI
cli = MatrixCLI()
result = cli.dispatch(["unknown"])
assert result["status"] == "error"
def test_dispatch_matrix_status(self):
from yuxi.channels.adapters.matrix.cli import MatrixCLI
cli = MatrixCLI()
result = cli.dispatch(["matrix", "status"])
assert result["status"] == "ok"
assert result["command"] == "matrix status"
def test_dispatch_matrix_account_list(self):
from yuxi.channels.adapters.matrix.cli import MatrixCLI
cli = MatrixCLI()
result = cli.dispatch(["matrix", "account", "list"])
assert result["status"] == "ok"
assert "account" in result["command"]
def test_format_cli_help(self):
from yuxi.channels.adapters.matrix.cli import format_cli_help
help_text = format_cli_help()
assert "matrix account" in help_text
assert "matrix doctor" in help_text
class TestMatrixDoctor:
@pytest.mark.asyncio
async def test_diagnose_valid_config(self):
from yuxi.channels.adapters.matrix.doctor import MatrixDoctor
doctor = MatrixDoctor({
"user_id": "@bot:example.org",
"homeserver": "https://example.org",
"access_token": "tok123",
})
issues = await doctor.diagnose()
assert len(issues) == 0
@pytest.mark.asyncio
async def test_diagnose_missing_auth(self):
from yuxi.channels.adapters.matrix.doctor import MatrixDoctor
doctor = MatrixDoctor({"user_id": "@bot:example.org"})
issues = await doctor.diagnose()
assert len(issues) > 0
assert any(i["category"] == "auth" for i in issues)
@pytest.mark.asyncio
async def test_diagnose_missing_user_id(self):
from yuxi.channels.adapters.matrix.doctor import MatrixDoctor
doctor = MatrixDoctor({"access_token": "tok123"})
issues = await doctor.diagnose()
assert any(i["severity"] == "error" for i in issues)
@pytest.mark.asyncio
async def test_repair_creates_crypto_dir(self, tmp_path):
from yuxi.channels.adapters.matrix.doctor import MatrixDoctor
crypto_dir = str(tmp_path / "crypto")
doctor = MatrixDoctor({
"user_id": "@bot:example.org",
"homeserver": "https://example.org",
"access_token": "tok123",
"crypto_store_dir": crypto_dir,
})
repairs = await doctor.repair()
assert any(r["category"] == "encryption" for r in repairs)
def test_get_summary(self):
from yuxi.channels.adapters.matrix.doctor import MatrixDoctor
doctor = MatrixDoctor({"user_id": "@bot:example.org"})
doctor._issues = [
{"severity": "error", "message": "test"},
{"severity": "warning", "message": "test"},
{"severity": "info", "message": "test"},
]
summary = doctor.get_summary()
assert summary["error"] == 1
assert summary["warning"] == 1
assert summary["info"] == 1
def test_has_critical_issues(self):
from yuxi.channels.adapters.matrix.doctor import MatrixDoctor
doctor = MatrixDoctor({"user_id": "@bot:example.org"})
doctor._issues = [{"severity": "error", "message": "critical"}]
assert doctor.has_critical_issues is True
doctor._issues = [{"severity": "info", "message": "ok"}]
assert doctor.has_critical_issues is False