"""Tests for the LoL poll manager + shared game-integration payload processing. Covers the runtime wiring that was previously missing: the orphaned ``LoLPoller`` is now driven by ``LoLPollManager``, and polled payloads flow through the same ``process_payload`` core the HTTP ingest route uses. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any import pytest from ledgrab.core.game_integration import runtime_state from ledgrab.core.game_integration.adapters.lol_adapter import LoLAdapter from ledgrab.core.game_integration.lol_poll_manager import LoLPollManager # ── Fakes ─────────────────────────────────────────────────────────────── class _FakeBus: def __init__(self) -> None: self.published: list[Any] = [] def publish(self, event: Any) -> None: self.published.append(event) @dataclass class _FakeConfig: id: str enabled: bool adapter_type: str adapter_config: dict = field(default_factory=dict) class _FakePoller: """Stand-in for LoLPoller so the manager test never spawns real threads.""" instances: list["_FakePoller"] = [] def __init__(self, adapter_config: dict, callback: Any) -> None: self.adapter_config = adapter_config self.callback = callback self.started = False self.stopped = False _FakePoller.instances.append(self) def start(self) -> None: self.started = True def stop(self) -> None: self.stopped = True @pytest.fixture def fake_poller(monkeypatch): _FakePoller.instances = [] monkeypatch.setattr("ledgrab.core.game_integration.lol_poll_manager.LoLPoller", _FakePoller) return _FakePoller def _lol(id: str = "lol1", enabled: bool = True, **cfg) -> _FakeConfig: return _FakeConfig(id=id, enabled=enabled, adapter_type="lol", adapter_config=cfg) # ── process_payload ───────────────────────────────────────────────────── class TestProcessPayload: def setup_method(self): runtime_state.cleanup_state("pp_test") def test_publishes_parsed_events_and_records_stats(self): bus = _FakeBus() payload = { "activePlayer": { "championStats": {"currentHealth": 50, "maxHealth": 100}, "summonerName": "Me", "level": 9, }, "allPlayers": [], } events = runtime_state.process_payload("pp_test", LoLAdapter, {}, payload, bus) assert len(events) >= 1 assert any(e.event_type == "health" and abs(e.value - 0.5) < 1e-6 for e in events) assert len(bus.published) == len(events) assert runtime_state.get_stats("pp_test")["event_count"] == len(events) def test_swallows_parse_errors(self): bus = _FakeBus() class _Boom: ADAPTER_TYPE = "boom" @classmethod def parse_payload(cls, *a): raise ValueError("bad frame") events = runtime_state.process_payload("pp_test", _Boom, {}, {}, bus) assert events == [] assert bus.published == [] # ── LoLPollManager lifecycle ──────────────────────────────────────────── class TestLoLPollManager: def test_sync_starts_poller_for_enabled_lol(self, fake_poller): mgr = LoLPollManager(_FakeBus()) mgr.sync([_lol()]) assert mgr.active_count == 1 assert fake_poller.instances[-1].started is True def test_sync_ignores_non_lol_and_disabled(self, fake_poller): mgr = LoLPollManager(_FakeBus()) mgr.sync( [ _FakeConfig("cs", True, "cs2", {}), _lol("off", enabled=False), ] ) assert mgr.active_count == 0 assert fake_poller.instances == [] def test_sync_is_idempotent_for_unchanged_config(self, fake_poller): mgr = LoLPollManager(_FakeBus()) mgr.sync([_lol(poll_interval_ms=500)]) first = fake_poller.instances[-1] mgr.sync([_lol(poll_interval_ms=500)]) assert mgr.active_count == 1 assert len(fake_poller.instances) == 1 # not restarted assert first.stopped is False def test_sync_restarts_on_config_change(self, fake_poller): mgr = LoLPollManager(_FakeBus()) mgr.sync([_lol(poll_interval_ms=500)]) first = fake_poller.instances[-1] mgr.sync([_lol(poll_interval_ms=1000)]) assert first.stopped is True assert len(fake_poller.instances) == 2 assert mgr.active_count == 1 def test_sync_stops_when_removed_or_disabled(self, fake_poller): mgr = LoLPollManager(_FakeBus()) mgr.sync([_lol()]) poller = fake_poller.instances[-1] mgr.sync([]) # integration gone assert poller.stopped is True assert mgr.active_count == 0 def test_stop_all(self, fake_poller): mgr = LoLPollManager(_FakeBus()) mgr.sync([_lol("a"), _lol("b")]) assert mgr.active_count == 2 mgr.stop_all() assert mgr.active_count == 0 assert all(p.stopped for p in fake_poller.instances) def test_callback_routes_polled_data_through_process_payload(self, fake_poller): bus = _FakeBus() mgr = LoLPollManager(bus) mgr.sync([_lol("cbtest")]) callback = fake_poller.instances[-1].callback callback( { "activePlayer": { "championStats": {"currentHealth": 80, "maxHealth": 100}, "summonerName": "Me", "level": 1, }, "allPlayers": [], } ) assert any(e.event_type == "health" for e in bus.published)