From e69228b7937e189b6b155cf4f9760e8d5fec997a Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Mon, 23 Mar 2026 17:58:54 -0400 Subject: [PATCH] test: improve event bus unit test coverage to 99% (Refs #1191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for previously uncovered code paths in infrastructure.events.bus: - TestSingletonHelpers: get_event_bus() singleton, init_event_bus_persistence() (idempotency and default-path behaviour), and module __getattr__ error path - TestEventBusPersistence: exception-swallowing in _persist_event() and replay() (lines 143-144 and 200-202) via patched contextmanager Coverage: 92.7% → 99.3% (38 tests, all passing). Co-Authored-By: Claude Sonnet 4.6 --- tests/infrastructure/test_event_bus.py | 121 ++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/tests/infrastructure/test_event_bus.py b/tests/infrastructure/test_event_bus.py index 41d673e3..6623c80f 100644 --- a/tests/infrastructure/test_event_bus.py +++ b/tests/infrastructure/test_event_bus.py @@ -1,10 +1,21 @@ """Tests for the async event bus (infrastructure.events.bus).""" import sqlite3 +from pathlib import Path +from unittest.mock import patch import pytest -from infrastructure.events.bus import Event, EventBus, emit, event_bus, on +import infrastructure.events.bus as bus_module +from infrastructure.events.bus import ( + Event, + EventBus, + emit, + event_bus, + get_event_bus, + init_event_bus_persistence, + on, +) class TestEvent: @@ -349,3 +360,111 @@ class TestEventBusPersistence: assert mode == "wal" finally: conn.close() + + async def test_persist_event_exception_is_swallowed(self, tmp_path): + """_persist_event must not propagate SQLite errors.""" + from unittest.mock import MagicMock + + bus = EventBus() + bus.enable_persistence(tmp_path / "events.db") + + # Make the INSERT raise an OperationalError + mock_conn = MagicMock() + mock_conn.execute.side_effect = sqlite3.OperationalError("simulated failure") + + from contextlib import contextmanager + + @contextmanager + def fake_ctx(): + yield mock_conn + + with patch.object(bus, "_get_persistence_conn", fake_ctx): + # Should not raise + bus._persist_event(Event(type="x", source="s")) + + async def test_replay_exception_returns_empty(self, tmp_path): + """replay() must return [] when SQLite query fails.""" + from unittest.mock import MagicMock + + bus = EventBus() + bus.enable_persistence(tmp_path / "events.db") + + mock_conn = MagicMock() + mock_conn.execute.side_effect = sqlite3.OperationalError("simulated failure") + + from contextlib import contextmanager + + @contextmanager + def fake_ctx(): + yield mock_conn + + with patch.object(bus, "_get_persistence_conn", fake_ctx): + result = bus.replay() + assert result == [] + + +# ── Singleton helpers ───────────────────────────────────────────────────────── + + +class TestSingletonHelpers: + """Test get_event_bus(), init_event_bus_persistence(), and module __getattr__.""" + + def test_get_event_bus_returns_same_instance(self): + """get_event_bus() is a true singleton.""" + a = get_event_bus() + b = get_event_bus() + assert a is b + + def test_module_event_bus_attr_is_singleton(self): + """Accessing bus_module.event_bus via __getattr__ returns the singleton.""" + assert bus_module.event_bus is get_event_bus() + + def test_module_getattr_unknown_raises(self): + """Accessing an unknown module attribute raises AttributeError.""" + with pytest.raises(AttributeError): + _ = bus_module.no_such_attr # type: ignore[attr-defined] + + def test_init_event_bus_persistence_sets_path(self, tmp_path): + """init_event_bus_persistence() enables persistence on the singleton.""" + bus = get_event_bus() + original_path = bus._persistence_db_path + try: + bus._persistence_db_path = None # reset for the test + db_path = tmp_path / "test_init.db" + init_event_bus_persistence(db_path) + assert bus._persistence_db_path == db_path + finally: + bus._persistence_db_path = original_path + + def test_init_event_bus_persistence_is_idempotent(self, tmp_path): + """Calling init_event_bus_persistence() twice keeps the first path.""" + bus = get_event_bus() + original_path = bus._persistence_db_path + try: + bus._persistence_db_path = None + first_path = tmp_path / "first.db" + second_path = tmp_path / "second.db" + init_event_bus_persistence(first_path) + init_event_bus_persistence(second_path) # should be ignored + assert bus._persistence_db_path == first_path + finally: + bus._persistence_db_path = original_path + + def test_init_event_bus_persistence_default_path(self): + """init_event_bus_persistence() uses 'data/events.db' when no path given.""" + bus = get_event_bus() + original_path = bus._persistence_db_path + try: + bus._persistence_db_path = None + # Patch enable_persistence to capture what path it receives + captured = {} + + def fake_enable(path: Path) -> None: + captured["path"] = path + + with patch.object(bus, "enable_persistence", side_effect=fake_enable): + init_event_bus_persistence() + + assert captured["path"] == Path("data/events.db") + finally: + bus._persistence_db_path = original_path -- 2.43.0