From 45b6edc6c0b7dbec97297573248262c0bc297c2b Mon Sep 17 00:00:00 2001 From: penguinboi Date: Mon, 9 Nov 2026 00:13:07 -0500 Subject: [PATCH] fix scheduler not starting --- matrix/bot.py | 10 ++++++---- tests/test_bot.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/matrix/bot.py b/matrix/bot.py index 67c7e24..05e48fb 100644 --- a/matrix/bot.py +++ b/matrix/bot.py @@ -5,7 +5,7 @@ from typing import Optional, Any -from nio import AsyncClient, Event, MatrixRoom +from nio import AsyncClient, Event, MatrixRoom, SyncResponse from .room import Room, make_room from .space import Space @@ -297,6 +297,7 @@ def _load_config(self, config: Config | str) -> None: self.register_command(self.help) self.client.add_event_callback(self._on_matrix_event, Event) + self.client.add_response_callback(self._on_sync, SyncResponse) self._auto_register_events() def start(self, *, config: Config | str) -> None: @@ -351,15 +352,16 @@ async def run(self) -> None: async def _wait_until_synced(self) -> None: await self._synced.wait() + async def _on_sync(self, _response: SyncResponse) -> None: + """Mark the bot as synced once a sync with the homeserver completes.""" + self._synced.set() + # MATRIX EVENTS async def on_message(self, room: Room, event: Event) -> None: await self._process_commands(room, event) async def _on_matrix_event(self, matrix_room: MatrixRoom, event: Event) -> None: - if not self._synced.is_set(): - self._synced.set() - # ignore bot events if event.sender == self.client.user: return diff --git a/tests/test_bot.py b/tests/test_bot.py index 99bb9cc..c33304a 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import AsyncMock, MagicMock, patch -from nio import MatrixRoom, RoomMessageText, LoginError +from nio import MatrixRoom, RoomMessageText, LoginError, SyncResponse from matrix import Bot, Config, Context, Extension, Room, Space from matrix.errors import ( @@ -720,6 +720,47 @@ def test_start_handles_keyboard_interrupt(caplog): bot._client.close.assert_awaited_once() +def test_load_config_registers_sync_response_callback(): + bot = Bot() + bot._load_config(Config(username="grace", password="grace1234")) + + assert any( + cb.func == bot._on_sync and cb.filter is SyncResponse + for cb in bot.client.response_callbacks + ) + + +@pytest.mark.asyncio +async def test_room_event_does_not_mark_synced(bot, room, event): + with patch.object(bot, "_dispatch_matrix_event", new_callable=AsyncMock): + await bot._on_matrix_event(room, event) + + assert not bot._synced.is_set() + + +@pytest.mark.asyncio +async def test_run_becomes_ready_after_first_sync_without_room_events(bot_with_token): + async def sync_forever(**_kwargs): + # A completed sync with no room events, then the loop keeps going. + await bot_with_token._on_sync(MagicMock(spec=SyncResponse)) + await asyncio.Event().wait() + + bot_with_token._client.sync_forever = AsyncMock(side_effect=sync_forever) + bot_with_token._on_ready = AsyncMock() + bot_with_token.scheduler.start = MagicMock() + + task = asyncio.create_task(bot_with_token.run()) + + await asyncio.wait_for(bot_with_token._synced.wait(), timeout=1.0) + await asyncio.sleep(0) + + task.cancel() + await asyncio.gather(task, return_exceptions=True) + + bot_with_token._on_ready.assert_awaited_once() + bot_with_token.scheduler.start.assert_called_once() + + @pytest.mark.asyncio async def test_on_ready_called_only_once(bot): # Prepare