from __future__ import annotations import asyncio from app.config import Settings from app.notify import FanoutNotifier, Notifier, WebhookNotifier, build_notifier from app.runtime import Runtime class RecordingNotifier(Notifier): def __init__(self, name: str = "rec", fail: bool = False) -> None: self.name = name self.fail = fail self.messages: list[tuple[str, str]] = [] async def send(self, level: str, message: str) -> None: if self.fail: raise RuntimeError("boom") self.messages.append((level, message)) async def test_fanout_sends_to_all() -> None: first = RecordingNotifier("a") second = RecordingNotifier("b") await FanoutNotifier([first, second]).send("trade", "hello") assert first.messages == [("trade", "hello")] assert second.messages == [("trade", "hello")] async def test_fanout_survives_a_failing_channel() -> None: bad = RecordingNotifier("bad", fail=True) good = RecordingNotifier("good") await FanoutNotifier([bad, good]).send("error", "still delivered") assert good.messages == [("error", "still delivered")] def test_build_notifier_none_without_config() -> None: assert build_notifier(Settings(telegram_bot_token="", notify_webhook_url="")) is None def test_build_notifier_telegram_requires_both_fields() -> None: assert build_notifier(Settings(telegram_bot_token="t", telegram_chat_id="")) is None notifier = build_notifier(Settings(telegram_bot_token="t", telegram_chat_id="c")) assert notifier is not None def test_build_notifier_webhook() -> None: notifier = build_notifier(Settings(notify_webhook_url="https://example.com/hook")) assert isinstance(notifier, FanoutNotifier) assert any(isinstance(n, WebhookNotifier) for n in notifier.notifiers) async def test_runtime_notifies_on_trade_events() -> None: recorder = RecordingNotifier() settings = Settings(telegram_bot_token="", notify_webhook_url="") runtime = Runtime(settings, notifier=recorder) runtime._broadcast({"type": "event", "data": {"level": "trade", "message": "opened long"}}) await asyncio.gather(*list(runtime._notify_tasks)) assert recorder.messages == [("trade", "opened long")] async def test_runtime_ignores_info_events() -> None: recorder = RecordingNotifier() settings = Settings(telegram_bot_token="", notify_webhook_url="") runtime = Runtime(settings, notifier=recorder) runtime._broadcast({"type": "event", "data": {"level": "info", "message": "started"}}) await asyncio.gather(*list(runtime._notify_tasks)) assert recorder.messages == []