from __future__ import annotations import importlib from types import SimpleNamespace from unittest.mock import AsyncMock from adapter.matrix.bot import MatrixBot, build_runtime from adapter.matrix.reconciliation import reconcile_startup_state from adapter.matrix.store import get_room_meta, get_user_meta, set_room_meta, set_user_meta from sdk.mock import MockPlatformClient def _room( room_id: str, name: str, members: list[str], *, parents: tuple[str, ...] = (), ): return SimpleNamespace( room_id=room_id, name=name, display_name=name, users={user_id: SimpleNamespace(user_id=user_id) for user_id in members}, space_parents=set(parents), ) async def test_reconcile_startup_state_restores_space_room_and_chat_bindings(): runtime = build_runtime(platform=MockPlatformClient()) client = SimpleNamespace( user_id="@bot:example.org", rooms={ "!space:example.org": _room( "!space:example.org", "Lambda - Alice", ["@bot:example.org", "@alice:example.org"], ), "!chat3:example.org": _room( "!chat3:example.org", "Чат 3", ["@bot:example.org", "@alice:example.org"], parents=("!space:example.org",), ), }, ) await reconcile_startup_state(client, runtime) user_meta = await get_user_meta(runtime.store, "@alice:example.org") assert user_meta is not None assert user_meta["space_id"] == "!space:example.org" assert user_meta["next_chat_index"] == 4 room_meta = await get_room_meta(runtime.store, "!chat3:example.org") assert room_meta is not None assert room_meta["room_type"] == "chat" assert room_meta["chat_id"] == "C3" assert room_meta["space_id"] == "!space:example.org" assert room_meta["matrix_user_id"] == "@alice:example.org" assert room_meta["platform_chat_id"] == "1" chats = await runtime.chat_mgr.list_active("@alice:example.org") assert [chat.chat_id for chat in chats] == ["C3"] assert [chat.surface_ref for chat in chats] == ["!chat3:example.org"] async def test_reconcile_startup_state_is_idempotent_with_existing_local_state(): runtime = build_runtime(platform=MockPlatformClient()) client = SimpleNamespace( user_id="@bot:example.org", rooms={ "!space:example.org": _room( "!space:example.org", "Lambda - Alice", ["@bot:example.org", "@alice:example.org"], ), "!chat3:example.org": _room( "!chat3:example.org", "Чат 3", ["@bot:example.org", "@alice:example.org"], parents=("!space:example.org",), ), }, ) await set_user_meta( runtime.store, "@alice:example.org", {"space_id": "!space:example.org", "next_chat_index": 8}, ) await set_room_meta( runtime.store, "!chat3:example.org", { "room_type": "chat", "chat_id": "C3", "display_name": "Existing name", "matrix_user_id": "@alice:example.org", "space_id": "!space:example.org", "platform_chat_id": "42", }, ) await runtime.chat_mgr.get_or_create( user_id="@alice:example.org", chat_id="C3", platform="matrix", surface_ref="!chat3:example.org", name="Existing name", ) await reconcile_startup_state(client, runtime) await reconcile_startup_state(client, runtime) user_meta = await get_user_meta(runtime.store, "@alice:example.org") assert user_meta == {"space_id": "!space:example.org", "next_chat_index": 8} room_meta = await get_room_meta(runtime.store, "!chat3:example.org") assert room_meta is not None assert room_meta["display_name"] == "Existing name" assert room_meta["platform_chat_id"] == "42" chats = await runtime.chat_mgr.list_active("@alice:example.org") assert len(chats) == 1 assert chats[0].chat_id == "C3" async def test_reconciliation_prevents_lazy_bootstrap_for_existing_room(): runtime = build_runtime(platform=MockPlatformClient()) client = SimpleNamespace( user_id="@bot:example.org", rooms={ "!space:example.org": _room( "!space:example.org", "Lambda - Alice", ["@bot:example.org", "@alice:example.org"], ), "!chat3:example.org": _room( "!chat3:example.org", "Чат 3", ["@bot:example.org", "@alice:example.org"], parents=("!space:example.org",), ), }, room_send=AsyncMock(), ) bot = MatrixBot(client=client, runtime=runtime) bot._bootstrap_unregistered_room = AsyncMock() runtime.dispatcher.dispatch = AsyncMock(return_value=[]) await reconcile_startup_state(client, runtime) await bot.on_room_message( SimpleNamespace(room_id="!chat3:example.org"), SimpleNamespace(sender="@alice:example.org", body="hello"), ) bot._bootstrap_unregistered_room.assert_not_awaited() runtime.dispatcher.dispatch.assert_awaited_once() async def test_matrix_main_runs_reconciliation_before_sync_forever(monkeypatch): bot_module = importlib.import_module("adapter.matrix.bot") runtime = SimpleNamespace(platform=SimpleNamespace(close=AsyncMock())) call_order: list[str] = [] class FakeAsyncClient: def __init__(self, *args, **kwargs): self.access_token = None self.callbacks = [] self.close = AsyncMock() self.sync_forever = AsyncMock(side_effect=self._sync_forever) async def _sync_forever(self, *args, **kwargs): call_order.append("sync_forever") async def login(self, *args, **kwargs): raise AssertionError("login should not be called when access token is provided") def add_event_callback(self, callback, event_type): self.callbacks.append((callback, event_type)) async def fake_prepare_live_sync(client): call_order.append("prepare_live_sync") return "s123" async def fake_reconcile_startup_state(client, runtime): call_order.append("reconcile_startup_state") monkeypatch.setenv("MATRIX_HOMESERVER", "https://matrix.example.org") monkeypatch.setenv("MATRIX_USER_ID", "@bot:example.org") monkeypatch.setenv("MATRIX_ACCESS_TOKEN", "token") monkeypatch.setattr(bot_module, "AsyncClient", FakeAsyncClient) monkeypatch.setattr(bot_module, "build_runtime", lambda **kwargs: runtime) monkeypatch.setattr(bot_module, "prepare_live_sync", fake_prepare_live_sync) monkeypatch.setattr(bot_module, "reconcile_startup_state", fake_reconcile_startup_state) await bot_module.main() assert call_order == [ "prepare_live_sync", "reconcile_startup_state", "sync_forever", ]