from __future__ import annotations from types import SimpleNamespace from unittest.mock import AsyncMock, patch import httpx import pytest from adapter.matrix.bot import MatrixBot, build_runtime from adapter.matrix.handlers.context_commands import ( make_handle_context, make_handle_load, make_handle_reset, make_handle_save, ) from adapter.matrix.store import get_load_pending, get_reset_pending, set_load_pending, set_reset_pending from core.protocol import IncomingCommand, OutgoingMessage from core.store import InMemoryStore from sdk.interface import MessageResponse from sdk.mock import MockPlatformClient from sdk.prototype_state import PrototypeStateStore class MatrixCommandPlatform(MockPlatformClient): def __init__(self) -> None: super().__init__() self._prototype_state = PrototypeStateStore() self._agent_api = object() self.send_message = AsyncMock( return_value=MessageResponse( message_id="msg-1", response="ok", tokens_used=0, finished=True, ) ) @pytest.mark.asyncio async def test_save_command_auto_name_records_session(): platform = MatrixCommandPlatform() store = InMemoryStore() handler = make_handle_save( agent_api=platform._agent_api, store=store, prototype_state=platform._prototype_state, ) event = IncomingCommand( user_id="u1", platform="matrix", chat_id="!room:example.org", command="save", args=[], ) result = await handler(event, None, platform, None, None) assert len(result) == 1 assert isinstance(result[0], OutgoingMessage) assert "Сохранение запущено" in result[0].text sessions = await platform._prototype_state.list_saved_sessions("u1") assert len(sessions) == 1 assert sessions[0]["name"].startswith("context-") @pytest.mark.asyncio async def test_save_command_with_name_uses_given_name(): platform = MatrixCommandPlatform() store = InMemoryStore() handler = make_handle_save( agent_api=platform._agent_api, store=store, prototype_state=platform._prototype_state, ) event = IncomingCommand( user_id="u1", platform="matrix", chat_id="!room:example.org", command="save", args=["my-session"], ) await handler(event, None, platform, None, None) sessions = await platform._prototype_state.list_saved_sessions("u1") assert [session["name"] for session in sessions] == ["my-session"] @pytest.mark.asyncio async def test_load_command_shows_numbered_list_and_sets_pending(): platform = MatrixCommandPlatform() runtime = build_runtime(platform=platform) await runtime.chat_mgr.get_or_create( user_id="u1", chat_id="C1", platform="matrix", surface_ref="!room:example.org", name="Chat 1", ) await platform._prototype_state.add_saved_session("u1", "session-a") await platform._prototype_state.add_saved_session("u1", "session-b") handler = make_handle_load(store=runtime.store, prototype_state=platform._prototype_state) event = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="load", args=[]) result = await handler(event, runtime.auth_mgr, platform, runtime.chat_mgr, runtime.settings_mgr) assert "1. session-a" in result[0].text assert "2. session-b" in result[0].text pending = await get_load_pending(runtime.store, "u1", "!room:example.org") assert pending is not None assert [session["name"] for session in pending["saves"]] == ["session-a", "session-b"] @pytest.mark.asyncio async def test_load_command_without_saved_sessions_reports_empty(): platform = MatrixCommandPlatform() store = InMemoryStore() handler = make_handle_load(store=store, prototype_state=platform._prototype_state) event = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="load", args=[]) result = await handler(event, None, platform, None, None) assert "Нет сохранённых сессий" in result[0].text @pytest.mark.asyncio async def test_reset_command_shows_dialog_and_sets_pending(): platform = MatrixCommandPlatform() runtime = build_runtime(platform=platform) await runtime.chat_mgr.get_or_create( user_id="u1", chat_id="C1", platform="matrix", surface_ref="!room:example.org", name="Chat 1", ) handler = make_handle_reset(store=runtime.store, agent_base_url="http://127.0.0.1:8000") event = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="reset", args=[]) result = await handler(event, runtime.auth_mgr, platform, runtime.chat_mgr, runtime.settings_mgr) assert "!yes" in result[0].text assert "!save" in result[0].text assert "!no" in result[0].text assert await get_reset_pending(runtime.store, "u1", "!room:example.org") == {"active": True} @pytest.mark.asyncio async def test_reset_endpoint_unavailable_reports_error(): with patch("adapter.matrix.handlers.context_commands.httpx.AsyncClient") as client_cls: client = client_cls.return_value client.__aenter__ = AsyncMock(return_value=client) client.__aexit__ = AsyncMock(return_value=False) client.post = AsyncMock(side_effect=httpx.ConnectError("refused")) from adapter.matrix.handlers.context_commands import _call_reset_endpoint result = await _call_reset_endpoint("http://127.0.0.1:8000", "!room:example.org") assert "недоступен" in result[0].text.lower() @pytest.mark.asyncio async def test_context_command_shows_current_snapshot(): platform = MatrixCommandPlatform() store = InMemoryStore() await platform._prototype_state.set_current_session("u1", "session-a") await platform._prototype_state.set_last_tokens_used("u1", 99) await platform._prototype_state.add_saved_session("u1", "session-a") handler = make_handle_context(store=store, prototype_state=platform._prototype_state) event = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="context", args=[]) result = await handler(event, None, platform, None, None) assert "Сессия: session-a" in result[0].text assert "Токены (последний ответ): 99" in result[0].text assert "session-a" in result[0].text @pytest.mark.asyncio async def test_bot_intercepts_numeric_load_selection(): platform = MatrixCommandPlatform() runtime = build_runtime(platform=platform) client = SimpleNamespace( user_id="@bot:example.org", room_send=AsyncMock(), ) bot = MatrixBot(client, runtime) await set_load_pending( runtime.store, "@alice:example.org", "!room:example.org", {"saves": [{"name": "session-a", "created_at": "2026-04-17T00:00:00+00:00"}]}, ) room = SimpleNamespace(room_id="!room:example.org") event = SimpleNamespace(sender="@alice:example.org", body="1") await bot.on_room_message(room, event) platform.send_message.assert_awaited_once() assert await platform._prototype_state.get_current_session("@alice:example.org") == "session-a" client.room_send.assert_awaited_once_with( "!room:example.org", "m.room.message", {"msgtype": "m.text", "body": "Загрузка: session-a"}, ) @pytest.mark.asyncio async def test_bot_intercepts_reset_yes_before_dispatch(): platform = MatrixCommandPlatform() runtime = build_runtime(platform=platform) client = SimpleNamespace( user_id="@bot:example.org", room_send=AsyncMock(), ) bot = MatrixBot(client, runtime) runtime.dispatcher.dispatch = AsyncMock() await set_reset_pending(runtime.store, "@alice:example.org", "!room:example.org", {"active": True}) room = SimpleNamespace(room_id="!room:example.org") event = SimpleNamespace(sender="@alice:example.org", body="!yes") with patch("adapter.matrix.handlers.context_commands.httpx.AsyncClient") as client_cls: http_client = client_cls.return_value http_client.__aenter__ = AsyncMock(return_value=http_client) http_client.__aexit__ = AsyncMock(return_value=False) http_client.post = AsyncMock(return_value=SimpleNamespace(status_code=200)) await bot.on_room_message(room, event) runtime.dispatcher.dispatch.assert_not_awaited() client.room_send.assert_awaited_once_with( "!room:example.org", "m.room.message", {"msgtype": "m.text", "body": "Контекст сброшен."}, )