"""[하드닝 R3] DT 청산 연속 실패 경보 태스크 — 강참조 보관·타임아웃·실패 로그. 목은 공통 종단(telegram_notifier.notify_system_warning)에 건다. 실패 경로는 예외 발생형. """ import asyncio import gc import logging import pytest from app.services.daytrading import scheduler as dts from app.services.telegram_notifier import TelegramNotifier class _SendBoom(Exception): pass @pytest.fixture(autouse=True) def _reset(): dts._alert_tasks.clear() dts._dt_exit_health["consecutive_failures"] = 0 dts._dt_exit_health["last_error"] = None yield dts._alert_tasks.clear() @pytest.mark.asyncio async def test_alert_task_strongly_referenced_until_done(monkeypatch): gate = asyncio.Event() sent = [] async def _fake(self, category, message): await gate.wait() sent.append((category, message)) monkeypatch.setattr(TelegramNotifier, "notify_system_warning", _fake) dts._notify_dt_warning_bg_v2("hello") assert len(dts._alert_tasks) == 1 gc.collect() assert len(dts._alert_tasks) == 1 # GC 후에도 참조 유지 gate.set() for _ in range(5): await asyncio.sleep(0) assert sent == [("DAYTRADING", "hello")] assert len(dts._alert_tasks) == 0 # 완료 후 제거 @pytest.mark.asyncio async def test_alert_via_exit_failure_threshold(monkeypatch): sent = [] async def _fake(self, category, message): sent.append(message) monkeypatch.setattr(TelegramNotifier, "notify_system_warning", _fake) for _ in range(dts.DT_EXIT_FAILURE_ALERT_THRESHOLD): dts._record_dt_exit_result_v2(False, "timeout") assert len(dts._alert_tasks) == 1 await asyncio.gather(*list(dts._alert_tasks)) await asyncio.sleep(0) assert len(sent) == 1 and "timeout" in sent[0] assert not dts._alert_tasks @pytest.mark.asyncio async def test_alert_send_exception_logged_error(monkeypatch, caplog): async def _boom(self, category, message): raise _SendBoom("telegram down") monkeypatch.setattr(TelegramNotifier, "notify_system_warning", _boom) with caplog.at_level(logging.ERROR, logger=dts.logger.name): dts._notify_dt_warning_bg_v2("msg-x") await asyncio.gather(*list(dts._alert_tasks)) assert any("경보 텔레그램 실패" in r.message and "msg-x" in r.message and r.levelno == logging.ERROR for r in caplog.records) await asyncio.sleep(0) assert not dts._alert_tasks @pytest.mark.asyncio async def test_alert_send_timeout_logged_error(monkeypatch, caplog): async def _hang(self, category, message): await asyncio.sleep(3600) monkeypatch.setattr(TelegramNotifier, "notify_system_warning", _hang) monkeypatch.setattr(dts, "DT_ALERT_SEND_TIMEOUT_SEC", 0.05) with caplog.at_level(logging.ERROR, logger=dts.logger.name): dts._notify_dt_warning_bg_v2("msg-t") await asyncio.wait_for(asyncio.gather(*list(dts._alert_tasks)), timeout=2.0) assert any("초과" in r.message and "msg-t" in r.message for r in caplog.records) def test_alert_without_running_loop_does_not_raise(caplog): with caplog.at_level(logging.ERROR, logger=dts.logger.name): dts._notify_dt_warning_bg_v2("no-loop") assert any("경보 태스크 생성 실패" in r.message for r in caplog.records) assert not dts._alert_tasks