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

2408 lines
92 KiB
Python
Raw Normal View History

"""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"