From c666d908dac825f8f221412a33cd324ca0031a8f Mon Sep 17 00:00:00 2001 From: Mikhail Putilovskij Date: Sun, 19 Apr 2026 17:23:07 +0300 Subject: [PATCH] fix: make matrix entry-room bootstrap idempotent --- adapter/matrix/bot.py | 32 ++++++++++++++++++++++++ tests/adapter/matrix/test_dispatcher.py | 33 +++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/adapter/matrix/bot.py b/adapter/matrix/bot.py index a792620..5b84b60 100644 --- a/adapter/matrix/bot.py +++ b/adapter/matrix/bot.py @@ -28,6 +28,7 @@ from adapter.matrix.store import ( clear_load_pending, get_load_pending, get_room_meta, + set_room_meta, set_pending_confirm, ) from core.auth import AuthManager @@ -154,6 +155,28 @@ class MatrixBot: if outgoing: await self._send_all(room.room_id, outgoing) return + elif room_meta.get("redirect_room_id"): + redirect_room_id = room_meta["redirect_room_id"] + redirect_chat_id = room_meta.get("redirect_chat_id", "рабочий чат") + await self._send_all( + room.room_id, + [ + OutgoingMessage( + chat_id=room.room_id, + text=( + f"Рабочий чат уже создан: {redirect_chat_id}. " + "Открой приглашённую комнату для продолжения." + ), + ) + ], + ) + logger.info( + "matrix_redirect_entry_room", + room_id=room.room_id, + redirect_room_id=redirect_room_id, + user=sender, + ) + return chat_id = await resolve_chat_id(self.runtime.store, room.room_id, sender) incoming = from_room_event(event, room_id=room.room_id, chat_id=chat_id) @@ -218,6 +241,15 @@ class MatrixBot: "m.room.message", {"msgtype": "m.text", "body": welcome}, ) + await set_room_meta( + self.runtime.store, + room.room_id, + { + "matrix_user_id": sender, + "redirect_room_id": created["chat_room_id"], + "redirect_chat_id": created["chat_id"], + }, + ) return [ OutgoingMessage( chat_id=room.room_id, diff --git a/tests/adapter/matrix/test_dispatcher.py b/tests/adapter/matrix/test_dispatcher.py index 6e20089..97308b6 100644 --- a/tests/adapter/matrix/test_dispatcher.py +++ b/tests/adapter/matrix/test_dispatcher.py @@ -260,6 +260,39 @@ async def test_unregistered_room_bootstraps_space_and_chat_on_first_message(): room_send_calls = client.room_send.await_args_list assert any(call.args[0] == "!chat1:example.org" for call in room_send_calls) assert any(call.args[0] == "!entry:example.org" for call in room_send_calls) + entry_meta = await get_room_meta(runtime.store, "!entry:example.org") + assert entry_meta == { + "matrix_user_id": "@alice:example.org", + "redirect_room_id": "!chat1:example.org", + "redirect_chat_id": "C1", + } + + +async def test_unregistered_room_second_message_reuses_existing_bootstrap(): + runtime = build_runtime(platform=MockPlatformClient()) + await set_user_meta(runtime.store, "@alice:example.org", {"next_chat_index": 1}) + space_resp = SimpleNamespace(room_id="!space:example.org") + chat_resp = SimpleNamespace(room_id="!chat1:example.org") + client = SimpleNamespace( + user_id="@bot:example.org", + room_create=AsyncMock(side_effect=[space_resp, chat_resp]), + room_put_state=AsyncMock(), + room_send=AsyncMock(), + ) + bot = MatrixBot(client, runtime) + room = SimpleNamespace(room_id="!entry:example.org", display_name="Entry") + + await bot.on_room_message(room, SimpleNamespace(sender="@alice:example.org", body="hello")) + await bot.on_room_message(room, SimpleNamespace(sender="@alice:example.org", body="hello again")) + + assert client.room_create.await_count == 2 + room_send_calls = client.room_send.await_args_list + assert any(call.args[0] == "!entry:example.org" for call in room_send_calls) + assert any( + call.args[0] == "!entry:example.org" + and "Рабочий чат уже создан: C1" in call.args[2]["body"] + for call in room_send_calls + ) async def test_unregistered_room_creates_new_chat_in_existing_space():