"""하드닝 R4 — MDD 단계 전이 텔레그램 재시도/참조 보관 회귀 테스트.""" import asyncio from unittest.mock import AsyncMock import pytest async def _drain(): import app.services.mdd_manager as m while m._alert_tasks: await asyncio.gather(*list(m._alert_tasks), return_exceptions=True) @pytest.fixture def env(monkeypatch): import app.services.mdd_manager as m import app.services.telegram_notifier as tn monkeypatch.setattr(m, "_alert_backoff_v2", lambda: 0.0) monkeypatch.setattr(m, "_alert_retries_v2", lambda: 2) mgr = m.MDDManager() monkeypatch.setattr(mgr, "_write_state_v2", AsyncMock()) return m, tn, mgr @pytest.mark.asyncio async def test_retries_on_exception_then_succeeds(env, monkeypatch): m, tn, mgr = env mock = AsyncMock(side_effect=[RuntimeError("boom"), False, None]) monkeypatch.setattr(tn.telegram_notifier, "notify_mdd_warning", mock) await mgr.update(100.0) r = await mgr.update(100.0 * (1 + (-10.5) / 100)) assert r["action"] == "REDUCE_AND_PAUSE" assert m._alert_tasks # 참조 보관 await _drain() assert mock.await_count == 3 @pytest.mark.asyncio async def test_final_failure_logged_error(env, monkeypatch, caplog): m, tn, mgr = env mock = AsyncMock(return_value=False) monkeypatch.setattr(tn.telegram_notifier, "notify_mdd_warning", mock) await mgr.update(100.0) r = await mgr.update(79.0) assert r["action"] == "FORCE_REDUCE" with caplog.at_level("ERROR"): await _drain() assert mock.await_count == m._alert_retries_v2() + 1 assert any("최종 실패" in rec.message for rec in caplog.records) assert not m._alert_tasks def test_no_running_loop_logs_error_not_raise(env, caplog): m, tn, mgr = env mgr.peak_balance = 100.0 with caplog.at_level("ERROR"): r = mgr._update_locked_v2(79.0) assert r["action"] == "FORCE_REDUCE" assert any("예약 실패" in rec.message for rec in caplog.records)