From 716dec5dfd727e57f8f89f81551abcac3ace000f Mon Sep 17 00:00:00 2001 From: Mikhail Putilovskij Date: Fri, 3 Apr 2026 12:27:42 +0300 Subject: [PATCH] test(01-05): cover matrix confirm flow round trip - assert room_id is preserved on !yes and !no callbacks - exercise send_outgoing to confirm and cancel with user+room scope --- tests/adapter/matrix/test_confirm.py | 50 +++++++-- tests/adapter/matrix/test_converter.py | 8 +- tests/adapter/matrix/test_send_outgoing.py | 118 +++++++++++++++++++-- 3 files changed, 160 insertions(+), 16 deletions(-) diff --git a/tests/adapter/matrix/test_confirm.py b/tests/adapter/matrix/test_confirm.py index 219f5fe..bf52613 100644 --- a/tests/adapter/matrix/test_confirm.py +++ b/tests/adapter/matrix/test_confirm.py @@ -19,7 +19,8 @@ async def test_mat09_yes_reads_pending_confirm(): await set_pending_confirm( store, - "C1", + "@alice:example.org", + "!confirm:example.org", { "action_id": "delete_file", "description": "Удалить файл config.yaml", @@ -31,16 +32,16 @@ async def test_mat09_yes_reads_pending_confirm(): event = IncomingCallback( user_id="@alice:example.org", platform="matrix", - chat_id="C1", + chat_id="C7", action="confirm", - payload={"source": "command", "command": "yes"}, + payload={"source": "command", "command": "yes", "room_id": "!confirm:example.org"}, ) result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr) assert len(result) == 1 assert isinstance(result[0], OutgoingMessage) assert "Удалить файл config.yaml" in result[0].text - assert await get_pending_confirm(store, "C1") is None + assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None async def test_no_clears_pending_confirm(): @@ -52,7 +53,8 @@ async def test_no_clears_pending_confirm(): await set_pending_confirm( store, - "C1", + "@alice:example.org", + "!confirm:example.org", { "action_id": "delete_file", "description": "Удалить файл", @@ -64,15 +66,15 @@ async def test_no_clears_pending_confirm(): event = IncomingCallback( user_id="@alice:example.org", platform="matrix", - chat_id="C1", + chat_id="C7", action="cancel", - payload={"source": "command", "command": "no"}, + payload={"source": "command", "command": "no", "room_id": "!confirm:example.org"}, ) result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr) assert len(result) == 1 assert "отменено" in result[0].text.lower() - assert await get_pending_confirm(store, "C1") is None + assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None async def test_yes_without_pending_returns_no_pending(): @@ -94,3 +96,35 @@ async def test_yes_without_pending_returns_no_pending(): assert len(result) == 1 assert "Нет ожидающих" in result[0].text + + +async def test_yes_falls_back_to_legacy_chat_key_without_room_payload(): + store = InMemoryStore() + platform = MockPlatformClient() + chat_mgr = ChatManager(platform, store) + auth_mgr = AuthManager(platform, store) + settings_mgr = SettingsManager(platform, store) + + await set_pending_confirm( + store, + "legacy-chat", + { + "action_id": "delete_file", + "description": "Legacy confirm", + "payload": {}, + }, + ) + + handler = make_handle_confirm(store) + event = IncomingCallback( + user_id="@alice:example.org", + platform="matrix", + chat_id="legacy-chat", + action="confirm", + payload={"source": "command", "command": "yes"}, + ) + result = await handler(event, auth_mgr, platform, chat_mgr, settings_mgr) + + assert len(result) == 1 + assert "Legacy confirm" in result[0].text + assert await get_pending_confirm(store, "legacy-chat") is None diff --git a/tests/adapter/matrix/test_converter.py b/tests/adapter/matrix/test_converter.py index 631b5fc..05bad59 100644 --- a/tests/adapter/matrix/test_converter.py +++ b/tests/adapter/matrix/test_converter.py @@ -68,15 +68,19 @@ async def test_skills_alias_to_settings_command(): async def test_yes_to_callback(): - result = from_room_event(text_event("!yes"), room_id="!r:m.org", chat_id="C1") + result = from_room_event(text_event("!yes"), room_id="!room:example.org", chat_id="C7") assert isinstance(result, IncomingCallback) assert result.action == "confirm" + assert result.chat_id == "C7" + assert result.payload["room_id"] == "!room:example.org" async def test_no_to_callback(): - result = from_room_event(text_event("!no"), room_id="!r:m.org", chat_id="C1") + result = from_room_event(text_event("!no"), room_id="!room:example.org", chat_id="C7") assert isinstance(result, IncomingCallback) assert result.action == "cancel" + assert result.chat_id == "C7" + assert result.payload["room_id"] == "!room:example.org" async def test_file_attachment(): diff --git a/tests/adapter/matrix/test_send_outgoing.py b/tests/adapter/matrix/test_send_outgoing.py index e0f3963..17eeefa 100644 --- a/tests/adapter/matrix/test_send_outgoing.py +++ b/tests/adapter/matrix/test_send_outgoing.py @@ -4,21 +4,28 @@ from types import SimpleNamespace from unittest.mock import AsyncMock from adapter.matrix.bot import send_outgoing -from adapter.matrix.store import get_pending_confirm +from adapter.matrix.converter import from_room_event +from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm +from adapter.matrix.store import get_pending_confirm, set_room_meta +from core.auth import AuthManager +from core.chat import ChatManager from core.protocol import OutgoingUI, UIButton +from core.settings import SettingsManager from core.store import InMemoryStore +from sdk.mock import MockPlatformClient async def test_mat06_outgoing_ui_renders_text_with_yes_no(): client = SimpleNamespace(room_send=AsyncMock()) store = InMemoryStore() + await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"}) event = OutgoingUI( - chat_id="C1", + chat_id="C7", text="Удалить файл?", buttons=[UIButton(label="Подтвердить", action="confirm")], ) - await send_outgoing(client, "!room:ex", event, store=store) + await send_outgoing(client, "!confirm:example.org", event, store=store) client.room_send.assert_awaited_once() body = client.room_send.call_args.args[2]["body"] @@ -31,22 +38,121 @@ async def test_mat06_outgoing_ui_renders_text_with_yes_no(): async def test_mat07_outgoing_ui_no_reaction_sent(): client = SimpleNamespace(room_send=AsyncMock()) store = InMemoryStore() + await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"}) event = OutgoingUI( - chat_id="C1", + chat_id="C7", text="Confirm action?", buttons=[UIButton(label="OK", action="confirm", payload={"id": 1})], ) - await send_outgoing(client, "!room:ex", event, store=store) + await send_outgoing(client, "!confirm:example.org", event, store=store) assert client.room_send.await_count == 1 assert client.room_send.call_args.args[1] == "m.room.message" for call in client.room_send.call_args_list: assert call.args[1] != "m.reaction" - pending = await get_pending_confirm(store, "!room:ex") + pending = await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") assert pending == { "action_id": "confirm", "description": "Confirm action?", "payload": {"id": 1}, } + + +async def test_outgoing_ui_yes_round_trip_uses_user_and_room_scope(): + client = SimpleNamespace(room_send=AsyncMock()) + store = InMemoryStore() + platform = MockPlatformClient() + chat_mgr = ChatManager(platform, store) + auth_mgr = AuthManager(platform, store) + settings_mgr = SettingsManager(platform, store) + await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"}) + await set_room_meta(store, "!other:example.org", {"matrix_user_id": "@bob:example.org"}) + + await send_outgoing( + client, + "!confirm:example.org", + OutgoingUI( + chat_id="C7", + text="Archive room", + buttons=[UIButton(label="Confirm", action="archive", payload={"id": 7})], + ), + store=store, + ) + await send_outgoing( + client, + "!other:example.org", + OutgoingUI( + chat_id="C8", + text="Keep other room", + buttons=[UIButton(label="Confirm", action="archive", payload={"id": 8})], + ), + store=store, + ) + + callback = from_room_event( + SimpleNamespace( + sender="@alice:example.org", + body="!yes", + event_id="$yes", + msgtype="m.text", + replyto_event_id=None, + ), + room_id="!confirm:example.org", + chat_id="C7", + ) + result = await make_handle_confirm(store)(callback, auth_mgr, platform, chat_mgr, settings_mgr) + + assert "Archive room" in result[0].text + assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None + assert await get_pending_confirm(store, "@bob:example.org", "!other:example.org") is not None + + +async def test_outgoing_ui_no_round_trip_uses_user_and_room_scope(): + client = SimpleNamespace(room_send=AsyncMock()) + store = InMemoryStore() + platform = MockPlatformClient() + chat_mgr = ChatManager(platform, store) + auth_mgr = AuthManager(platform, store) + settings_mgr = SettingsManager(platform, store) + await set_room_meta(store, "!confirm:example.org", {"matrix_user_id": "@alice:example.org"}) + await set_room_meta(store, "!other:example.org", {"matrix_user_id": "@bob:example.org"}) + + await send_outgoing( + client, + "!confirm:example.org", + OutgoingUI( + chat_id="C7", + text="Delete room", + buttons=[UIButton(label="Confirm", action="delete", payload={"id": 7})], + ), + store=store, + ) + await send_outgoing( + client, + "!other:example.org", + OutgoingUI( + chat_id="C8", + text="Keep other room", + buttons=[UIButton(label="Confirm", action="archive", payload={"id": 8})], + ), + store=store, + ) + + callback = from_room_event( + SimpleNamespace( + sender="@alice:example.org", + body="!no", + event_id="$no", + msgtype="m.text", + replyto_event_id=None, + ), + room_id="!confirm:example.org", + chat_id="C7", + ) + result = await make_handle_cancel(store)(callback, auth_mgr, platform, chat_mgr, settings_mgr) + + assert "отменено" in result[0].text.lower() + assert await get_pending_confirm(store, "@alice:example.org", "!confirm:example.org") is None + assert await get_pending_confirm(store, "@bob:example.org", "!other:example.org") is not None