from __future__ import annotations from types import SimpleNamespace from unittest.mock import AsyncMock from nio.api import RoomVisibility from nio.responses import SyncResponse from adapter.matrix.bot import MatrixBot, build_runtime, prepare_live_sync from adapter.matrix.handlers.auth import handle_invite from adapter.matrix.store import get_room_meta, get_user_meta, set_user_meta from core.protocol import IncomingCallback, IncomingCommand, OutgoingMessage from sdk.mock import MockPlatformClient async def test_matrix_dispatcher_registers_custom_handlers(): runtime = build_runtime(platform=MockPlatformClient()) current_chat_id = "C9" start = IncomingCommand(user_id="u1", platform="matrix", chat_id=current_chat_id, command="start") await runtime.dispatcher.dispatch(start) new = IncomingCommand( user_id="u1", platform="matrix", chat_id=current_chat_id, command="new", args=["Research"] ) result = await runtime.dispatcher.dispatch(new) assert any(isinstance(r, OutgoingMessage) and "Research" in r.text for r in result) chats = await runtime.chat_mgr.list_active("u1") assert [c.chat_id for c in chats] == ["C1"] assert [c.surface_ref for c in chats] == [current_chat_id] new2 = IncomingCommand( user_id="u1", platform="matrix", chat_id=current_chat_id, command="new", args=["Ops"] ) await runtime.dispatcher.dispatch(new2) chats = await runtime.chat_mgr.list_active("u1") assert [c.chat_id for c in chats] == ["C1", "C2"] skills = IncomingCommand( user_id="u1", platform="matrix", chat_id=current_chat_id, command="settings_skills" ) result = await runtime.dispatcher.dispatch(skills) assert any(isinstance(r, OutgoingMessage) and "!skill on/off" in r.text for r in result) toggle = IncomingCallback( user_id="u1", platform="matrix", chat_id="C1", action="toggle_skill", payload={"skill_index": 2}, ) result = await runtime.dispatcher.dispatch(toggle) assert any(isinstance(r, OutgoingMessage) and "fetch-url" in r.text for r in result) async def test_new_chat_creates_real_matrix_room_when_client_available(): client = SimpleNamespace( room_create=AsyncMock(return_value=SimpleNamespace(room_id="!r2:example")), room_put_state=AsyncMock(), room_invite=AsyncMock(), ) runtime = build_runtime(platform=MockPlatformClient(), client=client) await set_user_meta(runtime.store, "u1", {"space_id": "!space:example", "next_chat_index": 7}) start = IncomingCommand(user_id="u1", platform="matrix", chat_id="C3", command="start") await runtime.dispatcher.dispatch(start) new = IncomingCommand( user_id="u1", platform="matrix", chat_id="C3", command="new", args=["Research"], ) result = await runtime.dispatcher.dispatch(new) client.room_create.assert_awaited_once_with( name="Research", visibility=RoomVisibility.private, is_direct=False, invite=["u1"], ) client.room_put_state.assert_awaited_once() put_call = client.room_put_state.call_args assert put_call.kwargs.get("room_id") == "!space:example" or put_call.args[0] == "!space:example" chats = await runtime.chat_mgr.list_active("u1") assert [c.chat_id for c in chats] == ["C7"] assert [c.surface_ref for c in chats] == ["!r2:example"] assert any(isinstance(r, OutgoingMessage) and "Research" in r.text for r in result) async def test_invite_event_creates_space_and_chat_room(): runtime = build_runtime(platform=MockPlatformClient()) await set_user_meta(runtime.store, "@alice:example.org", {"next_chat_index": 4}) space_resp = SimpleNamespace(room_id="!space:example.org") chat_resp = SimpleNamespace(room_id="!chat1:example.org") client = SimpleNamespace( join=AsyncMock(), room_create=AsyncMock(side_effect=[space_resp, chat_resp]), room_put_state=AsyncMock(), room_invite=AsyncMock(), room_send=AsyncMock(), ) room = SimpleNamespace(room_id="!dm:example.org", display_name="Alice") event = SimpleNamespace(sender="@alice:example.org", membership="invite") await handle_invite( client, room, event, runtime.platform, runtime.store, runtime.auth_mgr, runtime.chat_mgr, ) assert client.room_create.await_count == 2 first_call = client.room_create.call_args_list[0] assert first_call.kwargs.get("space") is True or ( len(first_call.args) > 0 and first_call.kwargs.get("space") is True ) assert first_call.kwargs.get("visibility") is RoomVisibility.private assert first_call.kwargs.get("invite") == ["@alice:example.org"] second_call = client.room_create.call_args_list[1] assert second_call.kwargs.get("visibility") is RoomVisibility.private assert second_call.kwargs.get("invite") == ["@alice:example.org"] client.room_invite.assert_not_awaited() client.room_put_state.assert_awaited_once() put_state_call = client.room_put_state.call_args assert put_state_call.kwargs.get("event_type") == "m.space.child" or put_state_call.args[1] == "m.space.child" user_meta = await get_user_meta(runtime.store, "@alice:example.org") assert user_meta is not None assert user_meta.get("space_id") == "!space:example.org" room_meta = await get_room_meta(runtime.store, "!chat1:example.org") assert room_meta is not None assert room_meta["chat_id"] == "C4" assert room_meta["space_id"] == "!space:example.org" assert await runtime.auth_mgr.is_authenticated("@alice:example.org") is True assert user_meta.get("next_chat_index") == 5 client.room_send.assert_awaited_once() async def test_invite_event_is_idempotent_per_user(): runtime = build_runtime(platform=MockPlatformClient()) space_resp = SimpleNamespace(room_id="!space:example.org") chat_resp = SimpleNamespace(room_id="!chat1:example.org") client = SimpleNamespace( join=AsyncMock(), room_create=AsyncMock(side_effect=[space_resp, chat_resp]), room_put_state=AsyncMock(), room_invite=AsyncMock(), room_send=AsyncMock(), ) room = SimpleNamespace(room_id="!dm:example.org", display_name="Alice") event = SimpleNamespace(sender="@alice:example.org", membership="invite") await handle_invite( client, room, event, runtime.platform, runtime.store, runtime.auth_mgr, runtime.chat_mgr, ) await handle_invite( client, room, event, runtime.platform, runtime.store, runtime.auth_mgr, runtime.chat_mgr, ) assert client.room_create.await_count == 2 async def test_bot_ignores_its_own_messages(): runtime = build_runtime(platform=MockPlatformClient()) client = SimpleNamespace(user_id="@bot:example.org") bot = MatrixBot(client, runtime) bot._send_all = AsyncMock() runtime.dispatcher.dispatch = AsyncMock() room = SimpleNamespace(room_id="!dm:example.org") event = SimpleNamespace(sender="@bot:example.org", body="hello") await bot.on_room_message(room, event) runtime.dispatcher.dispatch.assert_not_awaited() bot._send_all.assert_not_awaited() async def test_mat11_settings_returns_dashboard(): runtime = build_runtime(platform=MockPlatformClient()) current_chat_id = "C9" start = IncomingCommand(user_id="u1", platform="matrix", chat_id=current_chat_id, command="start") await runtime.dispatcher.dispatch(start) settings_cmd = IncomingCommand( user_id="u1", platform="matrix", chat_id=current_chat_id, command="settings" ) result = await runtime.dispatcher.dispatch(settings_cmd) assert len(result) >= 1 text = result[0].text assert "Скиллы" in text or "скиллы" in text.lower() assert "Личность" in text assert "Безопасность" in text assert "Активные чаты" in text assert "Изменить" not in text assert "!connectors" not in text assert "!whoami" not in text async def test_mat12_help_returns_command_reference(): runtime = build_runtime(platform=MockPlatformClient()) result = await runtime.dispatcher.dispatch( IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="help") ) assert len(result) == 1 text = result[0].text assert "!new" in text assert "!rename" in text assert "!archive" in text assert "!settings" in text assert "!yes" in text async def test_prepare_live_sync_returns_next_batch_from_bootstrap_sync(): client = SimpleNamespace( sync=AsyncMock( return_value=SyncResponse( next_batch="s123", rooms={}, device_key_count={}, device_list=SimpleNamespace(changed=[], left=[]), to_device_events=[], presence_events=[], account_data_events=[], ) ) ) since = await prepare_live_sync(client) client.sync.assert_awaited_once_with(timeout=0, full_state=True) assert since == "s123"