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

2570 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, description="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):
import time
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({"threadBindings": {"idleHours": 0.001}})
binding = mgr.bind("!room:example.org", "$root123")
binding.last_activity = time.monotonic() - 100
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("access_token" in e["field"] or "password" 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