import asyncio import pytest from app.analysis.alerts import Alert from app.analysis.confluence import Cluster from app.analysis.levels import Level, LevelKind, Side from app.bars.models import Timeframe from app.config import Settings from app.runtime import Runtime def runtime(tmp_path, **overrides) -> Runtime: settings = Settings( manual_lines_path=tmp_path / "manual_lines.json", ntfy_topic=overrides.pop("ntfy_topic", ""), **overrides, ) return Runtime(settings) def alert() -> Alert: level = Level( "pd:high", LevelKind.HORIZONTAL, Timeframe.D1, Side.RESISTANCE, 16, 1, "PDH", 100, 5000, 0, None, 0, 100, 100, False, False, ) cluster = Cluster("cl_x", Side.RESISTANCE, 5000, 5000, 5000, 16, [level], 1.0) return Alert(cluster, "BEARISH ZONE /ES") def test_one_engine_serves_every_connection(tmp_path): # Previously each WebSocket built its own engine, so reloading the page # cleared the cooldown and the same zone alerted again immediately. instance = runtime(tmp_path) assert instance.alert_engine is not None assert instance.alert_engine.min_score == instance.settings.confluence_min_score def test_alerts_reach_subscribers(tmp_path): instance = runtime(tmp_path) queue: asyncio.Queue = asyncio.Queue(maxsize=10) instance.subscribers.add(queue) async def scenario(): instance.dispatch_alerts([alert()]) return queue.get_nowait() event = asyncio.run(scenario()) assert event["type"] == "alert" assert event["message"] == "BEARISH ZONE /ES" def test_ntfy_failure_does_not_propagate(tmp_path, monkeypatch): # send_ntfy used to be awaited inside the WebSocket loop, whose except # clause only caught disconnects — so a push outage killed the connection. instance = runtime(tmp_path, ntfy_topic="chart-test") async def explode(*args, **kwargs): raise RuntimeError("ntfy is down") monkeypatch.setattr("app.runtime.send_ntfy", explode) asyncio.run(instance.notify("anything")) def test_blank_topic_sends_nothing(tmp_path, monkeypatch): instance = runtime(tmp_path) calls = [] async def record(server, topic, message): calls.append(topic) monkeypatch.setattr("app.runtime.send_ntfy", record) asyncio.run(instance.notify("anything")) assert calls == [""] # send_ntfy itself is the one that short-circuits