ForcePilot/backend/test/unit/channels/test_channels_matrix_comprehensive.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

2408 lines
92 KiB
Python

"""Comprehensive unit tests for Matrix channel adapter — edge cases and uncovered modules.
Covers: dedupe, security, allowlist, rooms_config, crypto, auto_join,
directory, polls async, config_schema (pattern/enum/min/max/type),
accounts remaining, credentials encrypted, draft_stream, media_meta,
profile_sync edges, sync_state edges, sent_cache edges, SSRFGuard edges,
concurrency edges, formatter edges, normalizer edges, voice edges,
reaction_utils edges, send edges, location/sticker edges.
"""
import asyncio
import os
import tempfile
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.matrix.adapter import MatrixAdapter
from yuxi.channels.models import (
ChannelIdentity,
ChannelResponse,
ChannelType,
DeliveryResult,
EventType,
MessageType,
)
# =============================================================================
# EventDeduplicator
# =============================================================================
class TestEventDeduplicator:
def test_not_duplicate_first_seen(self):
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator()
assert d.is_duplicate("$evt1") is False
def test_duplicate_second_time(self):
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator()
d.is_duplicate("$evt1")
assert d.is_duplicate("$evt1") is True
def test_different_events_distinct(self):
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator()
d.is_duplicate("$evt1")
assert d.is_duplicate("$evt2") is False
def test_clear_removes_all(self):
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator()
d.is_duplicate("$evt1")
d.clear()
assert len(d) == 0
assert d.is_duplicate("$evt1") is False
def test_len_reflects_stored_count(self):
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator()
d.is_duplicate("$a")
d.is_duplicate("$b")
assert len(d) == 2
def test_ttl_expiration_evicts_old_entries(self):
import time
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator(ttl_seconds=0.01, max_size=100)
d.is_duplicate("$evt1")
time.sleep(0.02)
assert d.is_duplicate("$evt1") is False
def test_max_size_evicts_oldest(self):
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator(max_size=2, ttl_seconds=3600)
d.is_duplicate("$a")
d.is_duplicate("$b")
d.is_duplicate("$c")
assert len(d) <= 2
assert d.is_duplicate("$a") is False
def test_duplicate_moves_to_end(self):
import time
from yuxi.channels.adapters.matrix.dedupe import EventDeduplicator
d = EventDeduplicator(max_size=3, ttl_seconds=3600)
d.is_duplicate("$a")
d.is_duplicate("$b")
d.is_duplicate("$a")
d.is_duplicate("$c")
assert len(d) == 3
assert d.is_duplicate("$b") is True
# =============================================================================
# MatrixSecurityPolicy
# =============================================================================
class TestMatrixSecurityPolicy:
def test_dm_policy_open(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"dm": {"policy": "open"}})
assert p.evaluate_dm_access("@anyone:example.org") is True
def test_dm_policy_disabled(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"dm": {"policy": "disabled"}})
assert p.evaluate_dm_access("@anyone:example.org") is False
def test_dm_policy_allowlist_match(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"dm": {"policy": "allowlist", "allowFrom": ["@friend:example.org"]}})
assert p.evaluate_dm_access("@friend:example.org") is True
def test_dm_policy_allowlist_no_match(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"dm": {"policy": "allowlist", "allowFrom": ["@friend:example.org"]}})
assert p.evaluate_dm_access("@stranger:example.org") is False
def test_dm_policy_pairing_match(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"dm": {"policy": "pairing", "allowFrom": ["@partner:example.org"]}})
assert p.evaluate_dm_access("@partner:example.org") is True
def test_dm_policy_pairing_no_match(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"dm": {"policy": "pairing", "allowFrom": ["@partner:example.org"]}})
assert p.evaluate_dm_access("@other:example.org") is False
def test_group_policy_open(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"groupPolicy": "open"})
assert p.evaluate_group_access("@anyone:example.org", "!room:example.org") is True
def test_group_policy_disabled(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"groupPolicy": "disabled"})
assert p.evaluate_group_access("@anyone:example.org", "!room:example.org") is False
def test_group_policy_allowlist_match(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"groupPolicy": "allowlist", "groupAllowFrom": ["@member:example.org"]})
assert p.evaluate_group_access("@member:example.org", "!room:example.org") is True
def test_group_room_users_match(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"rooms": {"!room:example.org": {"users": ["@room_member:example.org"]}}})
assert p.evaluate_group_access("@room_member:example.org", "!room:example.org") is True
assert p.evaluate_group_access("@other:example.org", "!room:example.org") is False
def test_allowlist_only_mode(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({
"allowlistOnly": True,
"dm": {"policy": "open", "allowFrom": ["@approved:example.org"]},
})
assert p.evaluate_dm_access("@approved:example.org") is True
assert p.evaluate_dm_access("@other:example.org") is False
def test_should_skip_bot_true(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"allowBots": False})
assert p.should_skip_bot("@[bot]helper:example.org") is True
def test_should_skip_bot_false_when_allowed(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"allowBots": True})
assert p.should_skip_bot("@[bot]helper:example.org") is False
def test_should_skip_bot_none_config(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({})
assert p.should_skip_bot("@[bot]helper:example.org") is False
def test_should_skip_bot_not_bot(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"allowBots": False})
assert p.should_skip_bot("@human:example.org") is False
def test_context_visibility_all(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"contextVisibility": "all"})
assert p.is_context_visible("direct") is True
assert p.is_context_visible("group") is True
def test_context_visibility_none(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"contextVisibility": "none"})
assert p.is_context_visible("direct") is False
assert p.is_context_visible("group") is False
def test_context_visibility_dm(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"contextVisibility": "dm"})
assert p.is_context_visible("direct") is True
assert p.is_context_visible("group") is False
def test_context_visibility_group(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"contextVisibility": "group"})
assert p.is_context_visible("direct") is False
assert p.is_context_visible("group") is True
def test_allowlist_only_property(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"allowlistOnly": True})
assert p.allowlist_only is True
def test_context_visibility_property(self):
from yuxi.channels.adapters.matrix.security import MatrixSecurityPolicy
p = MatrixSecurityPolicy({"contextVisibility": "dm"})
assert p.context_visibility == "dm"
# =============================================================================
# allowlist
# =============================================================================
class TestAllowlist:
def test_normalize_mxid(self):
from yuxi.channels.adapters.matrix.allowlist import normalize_mxid
assert normalize_mxid("@User:Example.Org") == "@user:example.org"
assert normalize_mxid(" @alice:matrix.org ") == "@alice:matrix.org"
def test_match_user_empty_list(self):
from yuxi.channels.adapters.matrix.allowlist import match_user_in_list
assert match_user_in_list("@user:example.org", []) is False
assert match_user_in_list("@user:example.org", [" "]) is False
def test_match_user_wildcard(self):
from yuxi.channels.adapters.matrix.allowlist import match_user_in_list
assert match_user_in_list("@user:example.org", ["*"]) is True
assert match_user_in_list("@anyone:any.org", ["*"]) is True
def test_match_user_wildcard_domain(self):
from yuxi.channels.adapters.matrix.allowlist import match_user_in_list
assert match_user_in_list("@alice:matrix.org", ["@*:matrix.org"]) is True
assert match_user_in_list("@alice:other.org", ["@*:matrix.org"]) is False
def test_match_user_wildcard_domain_case(self):
from yuxi.channels.adapters.matrix.allowlist import match_user_in_list
assert match_user_in_list("@alice:MATRIX.ORG", ["@*:matrix.org"]) is True
def test_match_user_exact(self):
from yuxi.channels.adapters.matrix.allowlist import match_user_in_list
assert match_user_in_list("@alice:matrix.org", ["@alice:matrix.org"]) is True
assert match_user_in_list("@bob:matrix.org", ["@alice:matrix.org"]) is False
def test_is_likely_bot(self):
from yuxi.channels.adapters.matrix.allowlist import is_likely_bot
assert is_likely_bot("@[bot]helper:matrix.org") is True
assert is_likely_bot("@helper(bot):matrix.org") is True
assert is_likely_bot("@human:matrix.org") is False
assert is_likely_bot("") is False
# =============================================================================
# rooms_config
# =============================================================================
class TestRoomsConfig:
def test_get_room_config_existing(self):
from yuxi.channels.adapters.matrix.rooms_config import get_room_config
config = {"rooms": {"!room:example.org": {"allow": True}}}
assert get_room_config("!room:example.org", config) == {"allow": True}
def test_get_room_config_missing(self):
from yuxi.channels.adapters.matrix.rooms_config import get_room_config
assert get_room_config("!unknown:example.org", {}) == {}
def test_get_room_users(self):
from yuxi.channels.adapters.matrix.rooms_config import get_room_users
config = {"rooms": {"!room:example.org": {"users": ["@a:x", "@b:x"]}}}
assert get_room_users("!room:example.org", config) == ["@a:x", "@b:x"]
assert get_room_users("!unknown:example.org", {}) == []
def test_is_room_allowed(self):
from yuxi.channels.adapters.matrix.rooms_config import is_room_allowed
config = {"rooms": {"!room:example.org": {"allow": False}}}
assert is_room_allowed("!room:example.org", config) is False
assert is_room_allowed("!other:example.org", {}) is True
def test_get_room_require_mention_per_room(self):
from yuxi.channels.adapters.matrix.rooms_config import get_room_require_mention
config = {
"requireMention": True,
"rooms": {"!room:example.org": {"requireMention": False}},
}
assert get_room_require_mention("!room:example.org", config) is False
assert get_room_require_mention("!other:example.org", config) is True
def test_get_room_require_mention_default(self):
from yuxi.channels.adapters.matrix.rooms_config import get_room_require_mention
assert get_room_require_mention("!room:example.org", {}) is True
assert get_room_require_mention("!room:example.org", {"requireMention": False}) is False
# =============================================================================
# auto_join
# =============================================================================
class TestAutoJoin:
def test_always(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
assert should_auto_join("!room:example.org", {"autoJoin": "always"}) is True
def test_off(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
assert should_auto_join("!room:example.org", {"autoJoin": "off"}) is False
def test_allowlist_allowed(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
config = {"autoJoin": "allowlist", "rooms": {"!room:example.org": {"allow": True}}}
assert should_auto_join("!room:example.org", config) is True
def test_allowlist_not_allowed(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
config = {"autoJoin": "allowlist", "rooms": {"!room:example.org": {"allow": False}}}
assert should_auto_join("!room:example.org", config) is False
def test_allowlist_not_in_rooms(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
assert should_auto_join("!unknown:example.org", {"autoJoin": "allowlist"}) is True
def test_unknown_mode(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
assert should_auto_join("!room:example.org", {"autoJoin": "unknown"}) is False
def test_default_off(self):
from yuxi.channels.adapters.matrix.auto_join import should_auto_join
assert should_auto_join("!room:example.org", {}) is False
# =============================================================================
# crypto
# =============================================================================
class TestCrypto:
@pytest.mark.asyncio
async def test_import_room_keys_no_file(self, tmp_path):
from yuxi.channels.adapters.matrix.crypto import import_room_keys
mock_client = MagicMock()
await import_room_keys(mock_client, str(tmp_path))
@pytest.mark.asyncio
async def test_import_room_keys_with_file(self, tmp_path):
import json
from yuxi.channels.adapters.matrix.crypto import import_room_keys
keys_file = tmp_path / "room_keys.json"
keys_file.write_text(json.dumps({"rooms": {"!room:example.org": {"sessions": []}}}), encoding="utf-8")
mock_client = MagicMock()
mock_client.import_room_keys = AsyncMock()
await import_room_keys(mock_client, str(tmp_path))
mock_client.import_room_keys.assert_awaited_once()
@pytest.mark.asyncio
async def test_import_room_keys_corrupted_file(self, tmp_path):
from yuxi.channels.adapters.matrix.crypto import import_room_keys
keys_file = tmp_path / "room_keys.json"
keys_file.write_text("not json", encoding="utf-8")
mock_client = MagicMock()
mock_client.import_room_keys = AsyncMock()
await import_room_keys(mock_client, str(tmp_path))
mock_client.import_room_keys.assert_not_awaited()
@pytest.mark.asyncio
async def test_import_room_keys_unexpected_format(self, tmp_path):
import json
from yuxi.channels.adapters.matrix.crypto import import_room_keys
keys_file = tmp_path / "room_keys.json"
keys_file.write_text(json.dumps({"not_rooms": 1}), encoding="utf-8")
mock_client = MagicMock()
mock_client.import_room_keys = AsyncMock()
await import_room_keys(mock_client, str(tmp_path))
mock_client.import_room_keys.assert_not_awaited()
@pytest.mark.asyncio
async def test_export_room_keys_success(self, tmp_path):
from yuxi.channels.adapters.matrix.crypto import export_room_keys
mock_client = MagicMock()
mock_client.export_room_keys = AsyncMock()
result = await export_room_keys(mock_client, str(tmp_path))
assert result is True
@pytest.mark.asyncio
async def test_export_room_keys_failure(self, tmp_path):
from yuxi.channels.adapters.matrix.crypto import export_room_keys
mock_client = MagicMock()
mock_client.export_room_keys = AsyncMock(side_effect=Exception("fail"))
result = await export_room_keys(mock_client, str(tmp_path))
assert result is False
# =============================================================================
# directory (async)
# =============================================================================
class TestDirectory:
@pytest.mark.asyncio
async def test_search_users_success(self):
from yuxi.channels.adapters.matrix.directory import search_users
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.user_id = "@alice:example.org"
mock_result.display_name = "Alice"
mock_result.avatar_url = "mxc://avatar"
resp = MagicMock()
resp.results = [mock_result]
mock_client.search_user_directory = AsyncMock(return_value=resp)
results = await search_users(mock_client, "alice")
assert len(results) == 1
assert results[0]["user_id"] == "@alice:example.org"
@pytest.mark.asyncio
async def test_search_users_error(self):
from yuxi.channels.adapters.matrix.directory import search_users
mock_client = MagicMock()
mock_client.search_user_directory = AsyncMock(side_effect=Exception("fail"))
results = await search_users(mock_client, "alice")
assert results == []
@pytest.mark.asyncio
async def test_search_groups_success(self):
from yuxi.channels.adapters.matrix.directory import search_groups
mock_client = MagicMock()
mock_result = MagicMock()
mock_result.room_id = "!room:example.org"
mock_result.name = "Test Room"
mock_result.canonical_alias = "#test:example.org"
mock_result.topic = "A test room"
mock_result.num_joined_members = 42
mock_result.world_readable = True
mock_result.guest_can_join = False
resp = MagicMock()
resp.results = [mock_result]
mock_client.room_directory_search = AsyncMock(return_value=resp)
results = await search_groups(mock_client, "test")
assert len(results) == 1
assert results[0]["room_id"] == "!room:example.org"
assert results[0]["num_joined_members"] == 42
@pytest.mark.asyncio
async def test_search_groups_error(self):
from yuxi.channels.adapters.matrix.directory import search_groups
mock_client = MagicMock()
mock_client.room_directory_search = AsyncMock(side_effect=Exception("fail"))
results = await search_groups(mock_client, "test")
assert results == []
@pytest.mark.asyncio
async def test_resolve_room_alias_success(self):
from yuxi.channels.adapters.matrix.directory import resolve_room_alias
mock_client = MagicMock()
resp = MagicMock()
resp.room_id = "!room:example.org"
resp.servers = ["example.org"]
mock_client.room_resolve_alias = AsyncMock(return_value=resp)
result = await resolve_room_alias(mock_client, "#test:example.org")
assert result is not None
assert result["room_id"] == "!room:example.org"
@pytest.mark.asyncio
async def test_resolve_room_alias_error(self):
from yuxi.channels.adapters.matrix.directory import resolve_room_alias
mock_client = MagicMock()
mock_client.room_resolve_alias = AsyncMock(side_effect=Exception("fail"))
result = await resolve_room_alias(mock_client, "#test:example.org")
assert result is None
@pytest.mark.asyncio
async def test_list_public_rooms_success(self):
from yuxi.channels.adapters.matrix.directory import list_public_rooms
mock_client = MagicMock()
mock_room = MagicMock()
mock_room.room_id = "!pub:example.org"
mock_room.name = "Public Room"
mock_room.canonical_alias = "#pub:example.org"
mock_room.topic = "Welcome"
mock_room.num_joined_members = 10
mock_room.world_readable = True
resp = MagicMock()
resp.chunk = [mock_room]
mock_client.public_rooms = AsyncMock(return_value=resp)
results = await list_public_rooms(mock_client)
assert len(results) == 1
assert results[0]["room_id"] == "!pub:example.org"
@pytest.mark.asyncio
async def test_list_public_rooms_error(self):
from yuxi.channels.adapters.matrix.directory import list_public_rooms
mock_client = MagicMock()
mock_client.public_rooms = AsyncMock(side_effect=Exception("fail"))
results = await list_public_rooms(mock_client)
assert results == []
# =============================================================================
# Polls async
# =============================================================================
class TestPollsAsync:
@pytest.mark.asyncio
async def test_create_poll_success(self):
from yuxi.channels.adapters.matrix.polls import create_poll
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$poll001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await create_poll(mock_client, "!room:example.org", "Question?", ["A", "B"])
assert result.success is True
assert result.message_id == "$poll001"
@pytest.mark.asyncio
async def test_create_poll_failure(self):
from yuxi.channels.adapters.matrix.polls import create_poll
mock_client = MagicMock()
mock_client.room_send = AsyncMock(side_effect=Exception("fail"))
result = await create_poll(mock_client, "!room:example.org", "Q?", ["A"])
assert result.success is False
assert "fail" in result.error
@pytest.mark.asyncio
async def test_send_poll_response_success(self):
from yuxi.channels.adapters.matrix.polls import send_poll_response
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$vote001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_poll_response(mock_client, "!room:example.org", "$poll001", ["answer_0"])
assert result.success is True
assert result.message_id == "$vote001"
@pytest.mark.asyncio
async def test_send_poll_response_failure(self):
from yuxi.channels.adapters.matrix.polls import send_poll_response
mock_client = MagicMock()
mock_client.room_send = AsyncMock(side_effect=Exception("fail"))
result = await send_poll_response(mock_client, "!room:example.org", "$poll001", ["answer_0"])
assert result.success is False
@pytest.mark.asyncio
async def test_end_poll_success(self):
from yuxi.channels.adapters.matrix.polls import end_poll
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$end001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await end_poll(mock_client, "!room:example.org", "$poll001", summary="Done")
assert result.success is True
assert result.message_id == "$end001"
@pytest.mark.asyncio
async def test_end_poll_no_summary(self):
from yuxi.channels.adapters.matrix.polls import end_poll
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$end002"
mock_client.room_send = AsyncMock(return_value=resp)
result = await end_poll(mock_client, "!room:example.org", "$poll001")
assert result.success is True
@pytest.mark.asyncio
async def test_end_poll_failure(self):
from yuxi.channels.adapters.matrix.polls import end_poll
mock_client = MagicMock()
mock_client.room_send = AsyncMock(side_effect=Exception("fail"))
result = await end_poll(mock_client, "!room:example.org", "$poll001")
assert result.success is False
# =============================================================================
# ConfigSchema edges
# =============================================================================
class TestConfigSchemaEdges:
def test_validate_pattern_mismatch(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({"user_id": "not-a-valid-user-id"})
assert any("pattern" in e["error"] for e in errors)
def test_validate_enum_mismatch(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"autoJoin": "invalid",
})
assert any("invalid" in e["error"] for e in errors)
def test_validate_min_constraint(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"sync_timeout_ms": 0,
})
assert any("below" in e["error"] for e in errors)
def test_validate_max_constraint(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"textChunkLimit": 100000,
})
assert any("above" in e["error"] for e in errors)
def test_validate_wrong_type_int_for_str(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": 123,
"access_token": "tok",
})
assert any("expected str" in e["error"] for e in errors)
def test_validate_wrong_type_str_for_int(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"mediaMaxMb": "ten",
})
assert any("expected int" in e["error"] for e in errors)
def test_validate_encryption_requires_crypto_dir(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"encryption": True,
})
assert any("crypto_store_dir" in e["field"] for e in errors)
def test_validate_nested_section(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"dm": {"policy": "invalid"},
})
assert any("dm.policy" in e["field"] for e in errors)
def test_validate_float_type(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"probe_timeout_s": "slow",
})
assert any("expected float" in e["error"] for e in errors)
def test_validate_float_with_int_passes(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"probe_timeout_s": 10,
})
assert not any("probe_timeout_s" in e["field"] for e in errors)
def test_validate_bool_type(self):
from yuxi.channels.adapters.matrix.config_schema import validate_config
errors = validate_config({
"user_id": "@bot:example.org",
"access_token": "tok",
"enabled": "yes",
})
assert any("expected bool" in e["error"] for e in errors)
# =============================================================================
# Accounts remaining
# =============================================================================
class TestAccountsRemaining:
def test_resolve_room_account(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {
"accounts": {"bot1": {"user_id": "@bot1:example.org"}},
"rooms": {"!room:example.org": {"account": "bot1"}},
}
mgr = MultiAccountManager(config)
assert mgr.resolve_room_account("!room:example.org") == "bot1"
assert mgr.resolve_room_account("!unknown:example.org") is None
def test_promote_to_default(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {
"accounts": {
"bot1": {"user_id": "@bot1:example.org", "isDefault": True},
"bot2": {"user_id": "@bot2:example.org"},
},
}
mgr = MultiAccountManager(config)
assert mgr.promote_to_default("bot2") is True
assert mgr.default_account_id == "bot2"
def test_promote_nonexistent(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
mgr = MultiAccountManager({"accounts": {"bot1": {"user_id": "@bot1:example.org"}}})
assert mgr.promote_to_default("nonexistent") is False
def test_has_multi_account(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
mgr1 = MultiAccountManager({"accounts": {"bot1": {"user_id": "@bot1:example.org"}}})
assert mgr1.has_multi_account is False
mgr2 = MultiAccountManager({
"accounts": {
"bot1": {"user_id": "@bot1:example.org"},
"bot2": {"user_id": "@bot2:example.org"},
},
})
assert mgr2.has_multi_account is True
def test_default_account_none_when_empty(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
mgr = MultiAccountManager({})
assert mgr.default_account is None
assert mgr.account_count == 0
def test_matrix_account_properties(self):
from yuxi.channels.adapters.matrix.accounts import MatrixAccount
acc = MatrixAccount("acc1", {
"user_id": "@bot:matrix.org",
"homeserver": "https://custom.org",
"isDefault": True,
"enabled": False,
})
assert acc.user_id == "@bot:matrix.org"
assert acc.homeserver == "https://custom.org"
assert acc.is_default is True
assert acc.enabled is False
def test_matrix_account_repr(self):
from yuxi.channels.adapters.matrix.accounts import MatrixAccount
acc = MatrixAccount("acc1", {"user_id": "@bot:matrix.org", "homeserver": "https://matrix.org"})
r = repr(acc)
assert "acc1" in r
assert "@bot:matrix.org" in r
# =============================================================================
# Credentials encrypted
# =============================================================================
class TestCredentialsEncrypted:
def test_save_and_load_encrypted(self, tmp_path):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials, CredentialStore
os.environ["MATRIX_CREDENTIAL_KEY"] = "test-encryption-key-32bytes!!"
try:
store = CredentialStore(base_dir=str(tmp_path))
creds = MatrixCredentials(user_id="@bot:a", homeserver="https://a", access_token="secret123")
store.save(creds)
loaded = store.load("@bot:a")
assert loaded is not None
assert loaded.access_token == "secret123"
finally:
del os.environ["MATRIX_CREDENTIAL_KEY"]
def test_list_accounts(self, tmp_path):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials, CredentialStore
store = CredentialStore(base_dir=str(tmp_path))
store.save(MatrixCredentials(user_id="@bot1:a", homeserver="https://a", access_token="t1"))
store.save(MatrixCredentials(user_id="@bot2:a", homeserver="https://a", access_token="t2"))
accounts = store.list_accounts()
assert len(accounts) >= 1
def test_load_all(self, tmp_path):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials, CredentialStore
store = CredentialStore(base_dir=str(tmp_path))
store.save(MatrixCredentials(user_id="@bot:a", homeserver="https://a", access_token="t"))
loaded = store.load_all()
assert len(loaded) >= 1
def test_from_dict(self):
from yuxi.channels.adapters.matrix.credentials import MatrixCredentials
creds = MatrixCredentials.from_dict({
"user_id": "@bot:a",
"homeserver": "https://a",
"access_token": "tok",
"device_id": "dev1",
"display_name": "Bot",
})
assert creds.user_id == "@bot:a"
assert creds.device_id == "dev1"
assert creds.display_name == "Bot"
# =============================================================================
# DraftStream
# =============================================================================
class TestDraftStreamEdges:
def test_draft_session_start_update_finish(self):
from yuxi.channels.adapters.matrix.draft_stream import DraftStreamSession
s = DraftStreamSession("!room:example.org")
assert not s.is_active
s.start("$evt1")
assert s.is_active
assert s.draft_event_id == "$evt1"
s.update("Hello world")
assert s.current_text == "Hello world"
s.finish()
assert not s.is_active
def test_get_or_create_draft_reuse(self):
from yuxi.channels.adapters.matrix.draft_stream import get_or_create_draft, remove_draft
d1 = get_or_create_draft("!room:example.org")
d2 = get_or_create_draft("!room:example.org")
assert d1 is d2
remove_draft("!room:example.org")
def test_remove_nonexistent_does_not_raise(self):
from yuxi.channels.adapters.matrix.draft_stream import remove_draft
remove_draft("!nonexistent:example.org")
@pytest.mark.asyncio
async def test_send_live_marker_success(self):
from yuxi.channels.adapters.matrix.draft_stream import send_live_marker
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$live001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_live_marker(
mock_client, "!room:example.org", "$evt",
{"msgtype": "m.text", "body": "hello"},
finished=True,
)
assert result.success is True
@pytest.mark.asyncio
async def test_send_live_marker_failure(self):
from yuxi.channels.adapters.matrix.draft_stream import send_live_marker
mock_client = MagicMock()
mock_client.room_send = AsyncMock(side_effect=Exception("fail"))
result = await send_live_marker(
mock_client, "!room:example.org", "$evt",
{"msgtype": "m.text", "body": "hello"},
)
assert result.success is False
# =============================================================================
# MediaMeta
# =============================================================================
class TestMediaMetaEdges:
def test_extract_duration_bytes_wav(self):
from yuxi.channels.adapters.matrix.media_meta import extract_duration_bytes
header = (
b"RIFF" + (44).to_bytes(4, "little") + b"WAVEfmt " +
b"\x10\x00\x00\x00\x01\x00\x01\x00" +
(44100).to_bytes(4, "little") +
(88200).to_bytes(4, "little") +
b"\x02\x00\x10\x00" +
b"data" + (100).to_bytes(4, "little")
)
data = header + b"\x00" * 100
duration = extract_duration_bytes(data, "audio/wav")
assert duration >= 0
def test_extract_duration_bytes_unknown(self):
from yuxi.channels.adapters.matrix.media_meta import extract_duration_bytes
duration = extract_duration_bytes(b"random_bytes" * 10, "audio/unknown")
assert duration == 0
def test_extract_duration_bytes_empty(self):
from yuxi.channels.adapters.matrix.media_meta import extract_duration_bytes
duration = extract_duration_bytes(b"", "audio/ogg")
assert duration == 0
def test_detect_codec_from_mime_type(self):
from yuxi.channels.adapters.matrix.media_meta import _detect_codec
assert _detect_codec(MagicMock(), "audio/mp3") == "mp3"
assert _detect_codec(MagicMock(), "audio/ogg") == "ogg"
assert _detect_codec(MagicMock(), None) == "magicmock"
def test_detect_codec_from_class_name(self):
from yuxi.channels.adapters.matrix.media_meta import _detect_codec
mp3_mock = MagicMock()
type(mp3_mock).__name__ = "MP3"
assert _detect_codec(mp3_mock, None) == "mp3"
ogg_mock = MagicMock()
type(ogg_mock).__name__ = "OggVorbis"
assert _detect_codec(ogg_mock, None) == "vorbis"
flac_mock = MagicMock()
type(flac_mock).__name__ = "FLAC"
assert _detect_codec(flac_mock, None) == "flac"
wav_mock = MagicMock()
type(wav_mock).__name__ = "Wave"
assert _detect_codec(wav_mock, None) == "pcm"
opus_mock = MagicMock()
type(opus_mock).__name__ = "Opus"
assert _detect_codec(opus_mock, None) == "opus"
aac_mock = MagicMock()
type(aac_mock).__name__ = "AAC"
assert _detect_codec(aac_mock, None) == "aac"
# =============================================================================
# ProfileSync edges
# =============================================================================
class TestProfileSyncEdges:
def test_resolve_avatar_url_http_no_homeserver(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_avatar_url
result = resolve_avatar_url("https://cdn.example.org/avatar.png")
assert result == "https://cdn.example.org/avatar.png"
def test_resolve_avatar_url_invalid(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_avatar_url
assert resolve_avatar_url("not-a-url", "example.org") is None
def test_resolve_display_name_unknown(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_display_name
assert resolve_display_name({}) == "unknown"
def test_resolve_display_name_non_mxid(self):
from yuxi.channels.adapters.matrix.profile_sync import resolve_display_name
assert resolve_display_name({"user_id": "plain_user"}) == "plain_user"
def test_compare_profiles_no_changes(self):
from yuxi.channels.adapters.matrix.profile_sync import compare_profiles
current = {"displayname": "Same", "avatar_url": "mxc://same"}
incoming = {"displayname": "Same", "avatar_url": "mxc://same"}
changes = compare_profiles(current, incoming)
assert len(changes) == 0
def test_build_profile_update_empty(self):
from yuxi.channels.adapters.matrix.profile_sync import build_profile_update
assert build_profile_update() == {}
assert build_profile_update(display_name="", avatar_url="") == {}
def test_mxc_to_http_invalid(self):
from yuxi.channels.adapters.matrix.profile_sync import mxc_to_http
assert mxc_to_http("not-mxc", "https://example.org") == "not-mxc"
def test_mxc_to_http_plain_homeserver(self):
from yuxi.channels.adapters.matrix.profile_sync import mxc_to_http
url = mxc_to_http("mxc://example.org/abc123", "example.org")
assert url.startswith("https://example.org")
# =============================================================================
# SyncStateMachine edges
# =============================================================================
class TestSyncStateMachineEdges:
def test_can_transition_valid(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
assert sm.can_transition(SyncState.SYNCING) is True
assert sm.can_transition(SyncState.ERROR) is True
def test_can_transition_invalid(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
sm.to_healthy()
assert sm.can_transition(SyncState.PREPARED) is False
def test_transition_to_prepared(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
sm.to_stopped()
assert sm.to_prepared() is True
assert sm.state == SyncState.PREPARED
def test_should_stop_retry(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine
sm = SyncStateMachine()
sm.to_healthy()
for _ in range(sm._max_retries):
sm.to_error()
sm.to_reconnecting()
sm.to_error()
assert sm.should_stop_retry is True
def test_should_auto_recover_from_error(self):
import time
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine, SyncState
sm = SyncStateMachine()
sm.to_healthy()
sm.to_error()
assert sm.should_auto_recover is False
sm._last_error_time -= 120
assert sm.should_auto_recover is True
def test_should_auto_recover_not_in_error(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine
sm = SyncStateMachine()
sm.to_healthy()
assert sm.should_auto_recover is False
def test_is_active(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine
sm = SyncStateMachine()
assert sm.is_active is True
sm.to_healthy()
assert sm.is_active is True
sm.to_error()
assert sm.is_active is False
def test_error_clears_retry_on_healthy(self):
from yuxi.channels.adapters.matrix.sync_state import SyncStateMachine
sm = SyncStateMachine()
sm.to_healthy()
sm.to_error()
sm.to_reconnecting()
sm.to_error()
sm.to_reconnecting()
assert sm.retry_count == 2
sm.to_healthy()
assert sm.retry_count == 0
# =============================================================================
# SentMessageCache edges
# =============================================================================
class TestSentMessageCacheEdges:
def test_max_size_eviction(self):
from yuxi.channels.adapters.matrix.sent_cache import SentMessageCache
cache = SentMessageCache(max_size=2)
cache.add("!room:example.org", "hash1", "$evt1")
cache.add("!room:example.org", "hash2", "$evt2")
cache.add("!room:example.org", "hash3", "$evt3")
assert len(cache) <= 2
assert cache.get("!room:example.org", "hash1") is None
def test_hashing_deterministic(self):
from yuxi.channels.adapters.matrix.sent_cache import hash_content
h1 = hash_content("hello")
h2 = hash_content("hello")
assert h1 == h2
def test_hashing_different(self):
from yuxi.channels.adapters.matrix.sent_cache import hash_content
h1 = hash_content("hello")
h2 = hash_content("world")
assert h1 != h2
# =============================================================================
# SSRFGuard edges
# =============================================================================
class TestSSRFGuardEdges:
def test_is_private_cgnat_range(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("100.64.0.1") is True
assert SSRFGuard.is_private_host("100.127.255.255") is True
assert SSRFGuard.is_private_host("100.63.0.1") is False
assert SSRFGuard.is_private_host("100.128.0.1") is False
def test_is_private_ipv6_localhost(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("::1") is True
assert SSRFGuard.is_private_host("[::1]") is True
def test_is_private_zero(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("0.0.0.0") is True
def test_is_private_127_variants(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("127.0.0.1") is True
assert SSRFGuard.is_private_host("127.0.0.2") is True
assert SSRFGuard.is_private_host("127.255.255.255") is True
def test_is_private_172_range_boundaries(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
assert SSRFGuard.is_private_host("172.16.0.1") is True
assert SSRFGuard.is_private_host("172.31.255.255") is True
assert SSRFGuard.is_private_host("172.15.0.1") is False
assert SSRFGuard.is_private_host("172.32.0.1") is False
def test_validate_url_no_hostname(self):
from yuxi.channels.adapters.matrix.sync_store import SSRFGuard
error = SSRFGuard.validate_url("bad:///path")
assert error is not None
def test_parse_second_octet(self):
from yuxi.channels.adapters.matrix.sync_store import _parse_second_octet
assert _parse_second_octet("100.64.0.1") == 64
assert _parse_second_octet("invalid") == 0
assert _parse_second_octet("10") == 0
# =============================================================================
# Concurrency edges
# =============================================================================
class TestConcurrencyEdges:
def test_startup_serial_lock_release_without_acquire(self):
from yuxi.channels.adapters.matrix.concurrency import StartupSerialLock
lock = StartupSerialLock()
lock.release_startup()
assert lock.is_starting is False
def test_startup_serial_lock_double_release(self):
from yuxi.channels.adapters.matrix.concurrency import StartupSerialLock
lock = StartupSerialLock()
lock.acquire_startup()
lock.release_startup()
lock.release_startup()
assert lock.is_starting is False
@pytest.mark.asyncio
async def test_account_data_write_queue_drain_empty(self):
from yuxi.channels.adapters.matrix.concurrency import AccountDataWriteQueue
queue = AccountDataWriteQueue()
items = await queue.drain()
assert items == []
@pytest.mark.asyncio
async def test_account_data_write_queue_consumer(self):
import asyncio
from yuxi.channels.adapters.matrix.concurrency import AccountDataWriteQueue
results = []
async def write_fn(account_type, data):
results.append((account_type, data))
queue = AccountDataWriteQueue()
await queue.enqueue("m.direct", {"key": "val"})
task = queue.start_consumer(write_fn, interval=0.01)
await asyncio.sleep(0.05)
await queue.stop_consumer()
assert len(results) >= 0
# =============================================================================
# Formatter edges
# =============================================================================
class TestFormatterEdges:
def test_empty_string(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("")
assert result == ""
def test_multiple_code_blocks(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("```\na\n```\n```\nb\n```")
assert result.count("<pre><code>") == 2
def test_heading_level_1(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("# H1")
assert "<h1>H1</h1>" in result
def test_heading_level_6(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("###### H6")
assert "<h6>H6</h6>" in result
def test_mixed_unsafe_tags_and_safe(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html('<div>safe</div><script>bad</script>')
assert "safe" in result
assert "<script" not in result
def test_strips_object_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<object data='x'></object>")
assert "<object" not in result
def test_strips_embed_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<embed src='x'>")
assert "<embed" not in result
def test_strips_form_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<form action='x'></form>")
assert "<form" not in result
def test_strips_input_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<input type='text'>")
assert "<input" not in result
def test_strips_link_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<link rel='stylesheet'>")
assert "<link" not in result
def test_strips_meta_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<meta charset='utf-8'>")
assert "<meta" not in result
def test_strips_base_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<base href='http://evil.com'>")
assert "<base" not in result
def test_strips_applet_tag(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("<applet code='x'></applet>")
assert "<applet" not in result
def test_headings_escape_html(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("## <script>alert(1)</script>")
assert "&lt;script&gt;" in result
assert "<script>" not in result
def test_blockquote_escape_html(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("> <script>alert(1)</script>")
assert "&lt;script&gt;" in result
def test_list_items_escape_html(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html("- <script>alert(1)</script>")
assert "&lt;script&gt;" in result
def test_style_expression_stripped(self):
from yuxi.channels.adapters.matrix.formatter import markdown_to_matrix_html
result = markdown_to_matrix_html('<div style="expression(alert(1))">')
assert "expression" not in result
# =============================================================================
# Normalizer edges
# =============================================================================
class TestNormalizerEdges:
@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 test_edit_event_detection(self, adapter):
event = MagicMock()
event.sender = "@user:example.org"
event.body = "edited"
event.event_id = "$evt_edit"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"body": "edited text",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.replace",
"event_id": "$original",
},
"m.new_content": {
"body": "edited text",
"msgtype": "m.text",
},
},
}
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is not None
assert msg.event_type == EventType.MESSAGE_UPDATED
def test_formatted_text_event(self, adapter):
from nio import RoomMessageFormatted
event = MagicMock(spec=RoomMessageFormatted)
event.sender = "@user:example.org"
event.body = "hello"
event.formatted_body = "<b>hello</b>"
event.event_id = "$evt_formatted"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"msgtype": "m.text",
"body": "hello",
"formatted_body": "<b>hello</b>",
},
}
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is not None
assert msg.message_type == MessageType.TEXT
def test_require_mention_filters_when_not_mentioned(self, adapter):
adapter.config["requireMention"] = True
event = MagicMock()
event.sender = "@user:example.org"
event.body = "hello without bot mention"
event.event_id = "$evt_filtered"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"body": "hello without bot mention",
"msgtype": "m.text",
},
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg is None
def test_thread_events_skip_mention_filter(self, adapter):
adapter.config["requireMention"] = True
event = MagicMock()
event.sender = "@user:example.org"
event.body = "hello in thread"
event.event_id = "$evt_thread"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"body": "hello in thread",
"msgtype": "m.text",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$root",
},
},
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg is not None
def test_dm_skip_mention_filter(self, adapter):
adapter.config["requireMention"] = True
event = MagicMock()
event.sender = "@user:example.org"
event.body = "hello DM"
event.event_id = "$evt_dm"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"body": "hello DM",
"msgtype": "m.text",
},
}
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is not None
def test_mentions_from_url_pattern(self, adapter):
formatted = f'<a href="https://matrix.to/#/@alice:example.org">Alice</a>'
event = MagicMock()
event.sender = "@user:example.org"
event.body = "hey"
event.event_id = "$evt_url_mention"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"body": "hey",
"formatted_body": formatted,
"msgtype": "m.text",
},
}
msg = adapter._normalizer.normalize(event, "!room:example.org")
assert msg.mentions is not None
assert any("alice:example.org" in uid for uid in msg.mentions.mentioned_user_ids)
def test_no_timestamp_uses_current(self, adapter):
event = MagicMock()
event.sender = "@user:example.org"
event.body = "hello"
event.event_id = "$evt_no_ts"
event.server_timestamp = None
event.source = {
"content": {
"body": "hello",
"msgtype": "m.text",
},
}
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg is not None
assert msg.timestamp is not None
def test_resolve_chat_type_direct_via_direct_rooms(self, adapter):
from yuxi.channels.adapters.matrix.session import MatrixSessionHelper
assert adapter._session.resolve_chat_type("!dm:example.org") == "direct"
def test_empty_mentions_when_nothing(self, adapter):
event = MagicMock()
event.sender = "@user:example.org"
event.body = ""
event.event_id = "$evt_empty"
event.server_timestamp = 1700000000000
event.source = {
"content": {
"body": "",
"formatted_body": "",
"msgtype": "m.text",
},
}
msg = adapter._normalizer.normalize(event, "!dm:example.org")
assert msg.mentions is None
# =============================================================================
# Voice edges
# =============================================================================
class TestVoiceEdges:
def test_is_voice_message_not_audio_msgtype(self):
from yuxi.channels.adapters.matrix.voice import is_voice_message
assert is_voice_message({"content": {"msgtype": "m.text", "org.matrix.msc3245.voice": {}}}) is False
def test_is_voice_message_msc1767_no_duration(self):
from yuxi.channels.adapters.matrix.voice import is_voice_message
assert is_voice_message({
"content": {"msgtype": "m.audio", "org.matrix.msc1767.audio": {}},
}) is False
def test_extract_audio_info_no_info(self):
from yuxi.channels.adapters.matrix.voice import extract_audio_info
info = extract_audio_info({"content": {"msgtype": "m.audio"}})
assert info["duration_ms"] == 0
assert info["is_voice"] is False
@pytest.mark.asyncio
async def test_send_tts_message(self):
from yuxi.channels.adapters.matrix.voice import send_tts_message
mock_client = MagicMock()
resp = MagicMock()
resp.content_uri = "mxc://example.org/tts"
resp2 = MagicMock()
resp2.event_id = "$tts001"
mock_client.upload = AsyncMock(return_value=(resp, {}))
mock_client.room_send = AsyncMock(return_value=resp2)
result = await send_tts_message(
mock_client, "!room:example.org",
"Hello this is a TTS message",
b"audio_data",
)
assert result.success is True
assert result.message_id == "$tts001"
@pytest.mark.asyncio
async def test_send_voice_note_upload_failure(self):
from yuxi.channels.adapters.matrix.voice import send_voice_note
mock_client = MagicMock()
mock_client.upload = AsyncMock(side_effect=Exception("upload fail"))
result = await send_voice_note(mock_client, "!room:example.org", b"audio")
assert result.success is False
assert "upload" in result.error.lower()
# =============================================================================
# ReactionUtils edges
# =============================================================================
class TestReactionUtilsEdges:
def test_extract_reaction_no_source(self):
from yuxi.channels.adapters.matrix.reaction_utils import extract_reaction
event = MagicMock()
event.source = {}
assert extract_reaction(event) is None
def test_select_reaction_mapped_emoji(self):
from yuxi.channels.adapters.matrix.reaction_utils import select_reaction
available = [{"key": "👍"}, {"key": "❤️"}]
result = select_reaction(available, "+1")
assert result is not None
assert result["key"] == "👍"
def test_select_reaction_case_insensitive(self):
from yuxi.channels.adapters.matrix.reaction_utils import select_reaction
available = [{"key": "👍"}]
result = select_reaction(available, "+1")
assert result is not None
def test_build_batch_remove_single(self):
from yuxi.channels.adapters.matrix.reaction_utils import build_batch_remove
reactions = [{"key": "👍", "target_event_id": "$evt"}]
result = build_batch_remove(reactions, "$evt")
assert len(result) == 1
assert result[0]["m.relates_to"]["key"] == "👍"
def test_build_batch_remove_different_targets(self):
from yuxi.channels.adapters.matrix.reaction_utils import build_batch_remove
reactions = [
{"key": "👍", "target_event_id": "$evt1"},
{"key": "❤️", "target_event_id": "$evt2"},
]
result = build_batch_remove(reactions, "$evt1")
assert len(result) == 1
def test_summarize_reactions_empty(self):
from yuxi.channels.adapters.matrix.reaction_utils import summarize_reactions
assert summarize_reactions([]) == {}
def test_summarize_reactions_skip_empty_key(self):
from yuxi.channels.adapters.matrix.reaction_utils import summarize_reactions
events = [
{"sender": "@a:x", "key": "", "target_event_id": "$evt"},
{"sender": "@b:x", "key": "👍", "target_event_id": "$evt"},
]
result = summarize_reactions(events)
assert "$evt" in result
assert result["$evt"]["unique_reactors"] == 1
# =============================================================================
# Send edges
# =============================================================================
class TestSendEdges:
def test_classify_send_error_missing_token(self):
from yuxi.channels.adapters.matrix.send import classify_send_error
exc = Exception()
exc.errcode = "M_MISSING_TOKEN"
error_type, _ = classify_send_error(exc)
assert error_type == "auth"
@pytest.mark.asyncio
async def test_send_formatted_text_no_reply_when_off(self):
from yuxi.channels.adapters.matrix.send import send_formatted_text
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$evt"
mock_client.room_send = AsyncMock(return_value=resp)
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", reply_to_message_id="$original")
result = await send_formatted_text(mock_client, "!room", response, reply_to_mode="off")
assert result.success is True
@pytest.mark.asyncio
async def test_send_reaction_success(self):
from yuxi.channels.adapters.matrix.send import send_reaction
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$reaction001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_reaction(mock_client, "!room:example.org", "$target", "👍")
assert result.success is True
assert result.message_id == "$reaction001"
@pytest.mark.asyncio
async def test_send_thread_message_success(self):
from yuxi.channels.adapters.matrix.send import send_thread_message
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$thread001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_thread_message(mock_client, "!room:example.org", "$root", "thread reply")
assert result.success is True
assert result.message_id == "$thread001"
@pytest.mark.asyncio
async def test_send_location_success(self):
from yuxi.channels.adapters.matrix.send import send_location
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$loc001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_location(mock_client, "!room:example.org", 51.5, -0.12)
assert result.success is True
@pytest.mark.asyncio
async def test_send_sticker_success(self):
from yuxi.channels.adapters.matrix.send import send_sticker
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$sticker001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_sticker(mock_client, "!room:example.org", "mxc://example.org/sticker")
assert result.success is True
@pytest.mark.asyncio
async def test_send_formatted_media_upload_failure(self):
from yuxi.channels.adapters.matrix.send import send_formatted_media
mock_client = MagicMock()
mock_client.upload = AsyncMock(side_effect=Exception("upload fail"))
result = await send_formatted_media(mock_client, "!room", "image", b"data", "image/png")
assert result.success is False
assert "upload" in result.error.lower()
@pytest.mark.asyncio
async def test_send_stream_chunk_new_message(self):
from yuxi.channels.adapters.matrix.send import send_stream_chunk
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$stream001"
mock_client.room_send = AsyncMock(return_value=resp)
result = await send_stream_chunk(mock_client, "!room", "$msg1", "hello", finished=False, stream_state={})
assert result.success is True
@pytest.mark.asyncio
async def test_send_stream_chunk_delta_update(self):
from yuxi.channels.adapters.matrix.send import send_stream_chunk
mock_client = MagicMock()
resp = MagicMock()
resp.event_id = "$stream002"
mock_client.room_send = AsyncMock(return_value=resp)
stream_state = {"!room:$msg1": 5}
result = await send_stream_chunk(
mock_client, "!room", "$msg1",
"hello world", finished=False, stream_state=stream_state,
)
assert result.success is True
# =============================================================================
# Location edges
# =============================================================================
class TestLocationEdges:
def test_format_location_text_body_only(self):
from yuxi.channels.adapters.matrix.location import format_location_text
text = format_location_text("Some place", "")
assert "[位置]" in text
assert "Some place" in text
def test_format_location_text_uri_only(self):
from yuxi.channels.adapters.matrix.location import format_location_text
text = format_location_text("", "geo:1,2")
assert "[位置]" in text
assert "geo:1,2" in text
def test_format_location_text_empty(self):
from yuxi.channels.adapters.matrix.location import format_location_text
assert format_location_text("", "") == "[位置]"
def test_extract_geo_coordinates_with_params(self):
from yuxi.channels.adapters.matrix.location import extract_geo_coordinates
result = extract_geo_coordinates("geo:51.5,-0.12;u=35")
assert result is not None
assert result["lat"] == 51.5
def test_extract_geo_coordinates_malformed(self):
from yuxi.channels.adapters.matrix.location import extract_geo_coordinates
assert extract_geo_coordinates("geo:abc,def") is None
# =============================================================================
# Sticker edges
# =============================================================================
class TestStickerEdges:
def test_parse_sticker_no_body(self):
from yuxi.channels.adapters.matrix.sticker import parse_sticker
result = parse_sticker({"content": {"msgtype": "m.sticker", "url": "mxc://example.org/st"}})
assert result is not None
assert result["text"] == "[贴纸]"
def test_build_outbound_sticker_no_dimensions(self):
from yuxi.channels.adapters.matrix.sticker import build_outbound_sticker
result = build_outbound_sticker("mxc://example.org/st")
assert result["msgtype"] == "m.sticker"
assert "w" not in result["info"]
def test_build_outbound_sticker_dimensions_only_one_set(self):
from yuxi.channels.adapters.matrix.sticker import build_outbound_sticker
result = build_outbound_sticker("mxc://example.org/st", width=100, height=0)
assert "w" not in result["info"]
# =============================================================================
# Adapter edge cases
# =============================================================================
class TestAdapterReportError:
@pytest.mark.asyncio
async def test_check_mention_not_mentioned(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
strategy = GroupMentionStrategy({"user_id": "@bot:example.org"})
result = strategy.evaluate_room_policy(
"!room:example.org", "@user:example.org",
"hello", "",
)
assert result is False
# =============================================================================
# GroupMentions edges
# =============================================================================
class TestGroupMentionsEdges:
def test_get_room_mention_policy(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
config = {
"requireMention": True,
"rooms": {
"!room:example.org": {
"requireMention": False,
"allowedRoles": ["admin"],
"mentionExemptUsers": ["@admin:example.org"],
},
},
}
strategy = GroupMentionStrategy(config)
policy = strategy.get_room_mention_policy("!room:example.org")
assert policy["require_mention"] is False
assert policy["allowed_roles"] == ["admin"]
assert policy["mention_exempt_users"] == ["@admin:example.org"]
def test_get_room_mention_policy_defaults(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
strategy = GroupMentionStrategy({})
policy = strategy.get_room_mention_policy("!unknown:example.org")
assert policy["require_mention"] is True
assert policy["allowed_roles"] == []
def test_evaluate_room_policy_no_mention_not_required(self):
from yuxi.channels.adapters.matrix.group_mentions import GroupMentionStrategy
strategy = GroupMentionStrategy({"requireMention": False, "user_id": "@bot:example.org"})
assert strategy.evaluate_room_policy("!room:example.org", "@user:x", "hello", "") is True
# =============================================================================
# ConfigPatch edges
# =============================================================================
class TestConfigPatchEdges:
def test_apply_patch_with_lists(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch, apply_patch
config = {"user_id": "@bot:a"}
patch = MatrixAccountPatch.from_dict({"dm_allow_from": ["@a:x", "@b:x"]})
result = apply_patch(config, patch)
assert result["dm_allow_from"] == ["@a:x", "@b:x"]
def test_apply_patch_with_rooms(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch, apply_patch
config = {"user_id": "@bot:a", "rooms": {}}
patch = MatrixAccountPatch.from_dict({"rooms": {"!r:x": {"allow": False}}})
result = apply_patch(config, patch)
assert result["rooms"] == {"!r:x": {"allow": False}}
def test_merge_patches_empty_overlay(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch, merge_patches
base = MatrixAccountPatch(display_name="A")
overlay = MatrixAccountPatch()
merged = merge_patches(base, overlay)
assert merged.display_name == "A"
def test_to_dict_ignores_none(self):
from yuxi.channels.adapters.matrix.config_patch import MatrixAccountPatch
patch = MatrixAccountPatch(display_name="A", sync_timeout_ms=None)
d = patch.to_dict()
assert "display_name" in d
assert "sync_timeout_ms" not in d
# =============================================================================
# Encryption edges
# =============================================================================
class TestEncryptionEdges:
def test_check_crypto_store_ready_not_dir(self, tmp_path):
from yuxi.channels.adapters.matrix.encryption import check_crypto_store_ready
file_path = tmp_path / "not_a_dir"
file_path.write_text("content")
result = check_crypto_store_ready(str(file_path))
assert result["status"] == "unavailable"
def test_check_crypto_store_ready_empty(self, tmp_path):
from yuxi.channels.adapters.matrix.encryption import check_crypto_store_ready
empty_dir = tmp_path / "empty_store"
empty_dir.mkdir()
result = check_crypto_store_ready(str(empty_dir))
assert result["status"] == "empty"
def test_check_crypto_store_ready_with_keys(self, tmp_path):
from yuxi.channels.adapters.matrix.encryption import check_crypto_store_ready
store_dir = tmp_path / "store_with_keys"
store_dir.mkdir()
(store_dir / "room_keys.json").write_text('{"rooms":{}}')
result = check_crypto_store_ready(str(store_dir))
assert result["status"] == "ready"
def test_format_encryption_unavailable_error_no_crypto_dir(self):
from yuxi.channels.adapters.matrix.encryption import format_encryption_unavailable_error
msg = format_encryption_unavailable_error({"homeserver": "https://example.org"})
assert "Set 'crypto_store_dir'" in msg
@pytest.mark.asyncio
async def test_decrypt_event_list_result(self):
from yuxi.channels.adapters.matrix.encryption import decrypt_event
mock_client = MagicMock()
mock_client.decrypt_event = AsyncMock(return_value=[{"decrypted": True}])
result = await decrypt_event(mock_client, MagicMock(), "!room:example.org")
assert result is not None
assert result == {"decrypted": True}
@pytest.mark.asyncio
async def test_decrypt_event_dict_result(self):
from yuxi.channels.adapters.matrix.encryption import decrypt_event
mock_client = MagicMock()
mock_client.decrypt_event = AsyncMock(return_value={"decrypted": True})
result = await decrypt_event(mock_client, MagicMock(), "!room:example.org")
assert result is not None
assert result == {"decrypted": True}
@pytest.mark.asyncio
async def test_decrypt_event_failure(self):
from yuxi.channels.adapters.matrix.encryption import decrypt_event
mock_client = MagicMock()
mock_client.decrypt_event = AsyncMock(side_effect=Exception("fail"))
result = await decrypt_event(mock_client, MagicMock(), "!room:example.org")
assert result is None
@pytest.mark.asyncio
async def test_verify_device_success(self):
from yuxi.channels.adapters.matrix.encryption import verify_device
mock_client = MagicMock()
mock_client.verify_device = AsyncMock(return_value=True)
result = await verify_device(mock_client, "@user:x", "DEVICE1")
assert result["status"] == "ok"
assert result["verified"] is True
@pytest.mark.asyncio
async def test_verify_device_failure(self):
from yuxi.channels.adapters.matrix.encryption import verify_device
mock_client = MagicMock()
mock_client.verify_device = AsyncMock(side_effect=Exception("fail"))
result = await verify_device(mock_client, "@user:x", "DEVICE1")
assert result["status"] == "error"
@pytest.mark.asyncio
async def test_check_cross_signing_success(self):
from yuxi.channels.adapters.matrix.encryption import check_cross_signing
mock_client = MagicMock()
resp = MagicMock()
resp.cross_signing_keys = {"key1": "val1"}
mock_client.query_keys = AsyncMock(return_value=resp)
result = await check_cross_signing(mock_client)
assert result["status"] == "ok"
assert result["cross_signing_ready"] is True
@pytest.mark.asyncio
async def test_check_cross_signing_no_keys(self):
from yuxi.channels.adapters.matrix.encryption import check_cross_signing
mock_client = MagicMock()
resp = MagicMock()
resp.cross_signing_keys = {}
mock_client.query_keys = AsyncMock(return_value=resp)
result = await check_cross_signing(mock_client)
assert result["status"] == "ok"
assert result["cross_signing_ready"] is False
@pytest.mark.asyncio
async def test_check_cross_signing_failure(self):
from yuxi.channels.adapters.matrix.encryption import check_cross_signing
mock_client = MagicMock()
mock_client.query_keys = AsyncMock(side_effect=Exception("fail"))
result = await check_cross_signing(mock_client)
assert result["status"] == "error"
# =============================================================================
# SyncStore edges
# =============================================================================
class TestSyncStoreEdges:
def test_save_sync_token_empty(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_store import save_sync_token
save_sync_token(str(tmp_path), "")
assert True
def test_load_sync_token_corrupted(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_store import load_sync_token
token_file = tmp_path / "matrix_sync_token.json"
token_file.write_text("not json")
result = load_sync_token(str(tmp_path))
assert result is None
def test_sync_token_store_backup_recovery(self, tmp_path):
from yuxi.channels.adapters.matrix.sync_store import SyncTokenStore
store = SyncTokenStore(str(tmp_path), "@user:example.org")
store.save("s12345")
token_path = tmp_path / "sync_token_@user_example.org.json"
bak_path = tmp_path / "sync_token_@user_example.org.json.bak"
if token_path.exists() and bak_path.exists():
token_path.unlink()
loaded = store.load()
assert loaded == "s12345"
# =============================================================================
# Accounts edges
# =============================================================================
class TestAccountsEdges:
def test_active_client_tracker_transitions(self):
from yuxi.channels.adapters.matrix.accounts import ActiveClientTracker, ClientLifecycleState
tracker = ActiveClientTracker()
assert tracker.active_count == 0
tracker.register("acc1", MagicMock(), ClientLifecycleState.PREPARED)
assert tracker.get_state("acc1") == ClientLifecycleState.PREPARED
assert tracker.transition("acc1", ClientLifecycleState.STARTED) is True
assert tracker.active_count == 1
assert tracker.transition("acc1", ClientLifecycleState.PREPARED) is False
tracker.unregister("acc1")
assert tracker.get_state("acc1") == ClientLifecycleState.NONE
def test_active_client_tracker_list_active(self):
from yuxi.channels.adapters.matrix.accounts import ActiveClientTracker, ClientLifecycleState
tracker = ActiveClientTracker()
tracker.register("a", MagicMock(), ClientLifecycleState.STARTED)
tracker.register("b", MagicMock(), ClientLifecycleState.PREPARED)
assert tracker.list_active() == ["a"]
assert tracker.list_all_ids() == ["a", "b"]
def test_active_client_tracker_invalid_transition(self):
from yuxi.channels.adapters.matrix.accounts import ActiveClientTracker, ClientLifecycleState
tracker = ActiveClientTracker()
tracker.register("acc", MagicMock(), ClientLifecycleState.NONE)
assert tracker.transition("acc", ClientLifecycleState.STARTED) is False
def test_multi_account_manager_default_account_fallback(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
config = {"accounts": {"bot1": {"user_id": "@bot1:example.org"}}}
mgr = MultiAccountManager(config)
assert mgr.default_account_id == "bot1"
def test_multi_account_manager_empty(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
mgr = MultiAccountManager({"accounts": {}})
assert mgr.default_account is None
assert mgr.account_count == 0
def test_multi_account_manager_no_accounts_key(self):
from yuxi.channels.adapters.matrix.accounts import MultiAccountManager
mgr = MultiAccountManager({})
assert mgr.default_account is None
# =============================================================================
# Commands edges
# =============================================================================
class TestCommandsEdges:
def test_detect_slash_command_with_args_model(self):
from yuxi.channels.adapters.matrix.commands import detect_slash_command
result = detect_slash_command("/model gpt-4")
assert result is not None
assert result["command"] == "/model"
assert result["args"].get("model_name") == "gpt-4"
def test_detect_slash_command_proxy_no_args(self):
from yuxi.channels.adapters.matrix.commands import detect_slash_command
result = detect_slash_command("/proxy")
assert result is not None
assert result["command"] == "/proxy"
assert "spawn" not in result["args"] or not result["args"].get("spawn")
def test_is_slash_command_with_extra_whitespace(self):
from yuxi.channels.adapters.matrix.commands import is_slash_command
assert is_slash_command(" /help") is False
assert is_slash_command("/help ") is True
def test_command_list_all_covered(self):
from yuxi.channels.adapters.matrix.commands import get_command_list
cmds = get_command_list()
assert len(cmds) >= 9
def test_format_help_contains_all_commands(self):
from yuxi.channels.adapters.matrix.commands import format_help_text
text = format_help_text()
for cmd in ["/new", "/reset", "/model", "/help", "/status", "/channels", "/proxy", "/skill", "/room_info"]:
assert cmd in text
# =============================================================================
# ThreadBinding edges
# =============================================================================
class TestThreadBindingEdges:
def test_thread_binding_touch_updates_activity(self):
import time
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBinding
b = ThreadBinding("!room:example.org", "$root")
old_activity = b.last_activity
time.sleep(0.001)
b.touch()
assert b.last_activity > old_activity
def test_thread_binding_properties_init(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBinding
b = ThreadBinding("!room:example.org", "$root", session_key="s1")
assert b.room_id == "!room:example.org"
assert b.root_event_id == "$root"
assert b.session_key == "s1"
assert b.bound_at > 0
assert b.last_activity > 0
def test_binding_manager_max_age_expiry(self):
import time
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({"threadBindings": {"maxAgeHours": 0.001}})
binding = mgr.bind("!room:example.org", "$root")
binding.bound_at = time.monotonic() - 100
expired = mgr.check_timeouts()
assert len(expired) == 1
def test_binding_manager_get_room_bindings(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({})
mgr.bind("!room1:example.org", "$root1")
mgr.bind("!room2:example.org", "$root2")
bindings = mgr.get_room_bindings("!room1:example.org")
assert len(bindings) == 1
def test_binding_manager_list_bindings(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({})
mgr.bind("!room:example.org", "$root1")
mgr.bind("!room:example.org", "$root2")
assert len(mgr.list_bindings()) == 2
def test_binding_manager_config_disabled(self):
from yuxi.channels.adapters.matrix.thread_bindings import ThreadBindingManager
mgr = ThreadBindingManager({"threadBindings": {"enabled": False, "spawnSubagentSessions": True}})
b = mgr.bind("!room:example.org", "$root")
assert b.is_subagent is True
# =============================================================================
# ACPBinding edges
# =============================================================================
class TestACPBindingsEdges:
@pytest.mark.asyncio
async def test_acp_binding_spawn(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBinding
b = ACPBinding("!room:example.org", "$root", acp_session_id="acp1")
assert not b.bound
result = await b.spawn(MagicMock())
assert b.bound
assert result["status"] == "spawned"
@pytest.mark.asyncio
async def test_acp_binding_bind(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBinding
b = ACPBinding("!room:example.org", "$root")
result = await b.bind(MagicMock())
assert result["status"] == "bound"
@pytest.mark.asyncio
async def test_acp_binding_already_bound(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBinding
b = ACPBinding("!room:example.org", "$root")
b.bound = True
result = await b.bind(MagicMock())
assert result["status"] == "already_bound"
@pytest.mark.asyncio
async def test_acp_binding_unbind(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBinding
b = ACPBinding("!room:example.org", "$root")
b.bound = True
result = await b.unbind()
assert not b.bound
assert result["status"] == "unbound"
def test_acp_binding_to_dict(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBinding
b = ACPBinding("!room:example.org", "$root", "sess1", "thread")
d = b.to_dict()
assert d["room_id"] == "!room:example.org"
assert d["acp_session_id"] == "sess1"
def test_acp_manager_create_no_thread_root(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBindingManager
mgr = ACPBindingManager({})
b = mgr.create_binding("!room:example.org", "")
assert b.room_id == "!room:example.org"
def test_acp_manager_remove_nonexistent(self):
from yuxi.channels.adapters.matrix.acp_bindings import ACPBindingManager
mgr = ACPBindingManager({})
assert mgr.remove("!room:example.org", "$nonexistent") is False
# =============================================================================
# EnvVars edges
# =============================================================================
class TestEnvVarsEdges:
def test_resolve_env_vars_nested_key(self):
import os
from yuxi.channels.adapters.matrix.env_vars import resolve_env_vars
os.environ["MATRIX_DM_POLICY"] = "open"
try:
result = resolve_env_vars({"user_id": "@bot:example.org"})
assert "dm" in result
assert result["dm"]["policy"] == "open"
finally:
del os.environ["MATRIX_DM_POLICY"]
def test_coerce_type_float(self):
from yuxi.channels.adapters.matrix.env_vars import _coerce_type
assert _coerce_type("3.14", 1.0) == 3.14
def test_coerce_type_float_invalid(self):
from yuxi.channels.adapters.matrix.env_vars import _coerce_type
assert _coerce_type("abc", 1.0) == "abc"
def test_coerce_type_none_existing(self):
from yuxi.channels.adapters.matrix.env_vars import _coerce_type
assert _coerce_type("hello", None) == "hello"
# =============================================================================
# Probe edges
# =============================================================================
class TestProbeEdges:
@pytest.mark.asyncio
async def test_check_backup_health_all_unreachable(self):
from yuxi.channels.adapters.matrix.probe import _check_backup_health
mock_client = MagicMock()
mock_client.keys_upload = AsyncMock(side_effect=Exception())
mock_client.keys_query = AsyncMock(side_effect=Exception())
mock_client.query_keys = AsyncMock(side_effect=Exception())
result = await _check_backup_health(mock_client, 5)
assert result["status"] == "unknown"
def test_describe_backup_state_unknown_code(self):
from yuxi.channels.adapters.matrix.probe import describe_backup_state
result = describe_backup_state(999)
assert result == "unknown_state_999"
@pytest.mark.asyncio
async def test_probe_matrix_timeout(self):
import asyncio
from yuxi.channels.adapters.matrix.probe import probe_matrix
mock_client = MagicMock()
mock_client.versions = AsyncMock(side_effect=asyncio.TimeoutError())
mock_client.homeserver = "https://example.org"
mock_client.user_id = "@bot:example.org"
result = await probe_matrix(mock_client, timeout_s=0.1)
assert result.status == "unhealthy"
@pytest.mark.asyncio
async def test_probe_matrix_connection_error(self):
from yuxi.channels.adapters.matrix.probe import probe_matrix
mock_client = MagicMock()
mock_client.versions = AsyncMock(side_effect=ConnectionError("refused"))
mock_client.homeserver = "https://example.org"
mock_client.user_id = "@bot:example.org"
result = await probe_matrix(mock_client, timeout_s=10)
assert result.status == "unhealthy"
@pytest.mark.asyncio
async def test_probe_matrix_os_error(self):
from yuxi.channels.adapters.matrix.probe import probe_matrix
mock_client = MagicMock()
mock_client.versions = AsyncMock(side_effect=OSError("network unreachable"))
mock_client.homeserver = "https://example.org"
mock_client.user_id = "@bot:example.org"
result = await probe_matrix(mock_client, timeout_s=10)
assert result.status == "unhealthy"
@pytest.mark.asyncio
async def test_probe_matrix_generic_error(self):
from yuxi.channels.adapters.matrix.probe import probe_matrix
mock_client = MagicMock()
mock_client.versions = AsyncMock(side_effect=RuntimeError("unexpected"))
mock_client.homeserver = "https://example.org"
mock_client.user_id = "@bot:example.org"
result = await probe_matrix(mock_client, timeout_s=10)
assert result.status == "degraded"