"""R2 2026-10-09: 적응형 모드 갱신 장기 실패 시 Defensive 강등 + 경보 (Crisis fail-open 방지).""" import time import pytest import app.services.adaptive_strategy as ad from app.services.adaptive_strategy import TradingMode @pytest.fixture(autouse=True) def _state(monkeypatch): monkeypatch.setattr(ad, "_current_params", ad.get_adaptive_params( ad.MarketCondition(trading_mode=TradingMode.NORMAL.value))) monkeypatch.setattr(ad, "_last_update_ts", time.monotonic()) monkeypatch.setattr(ad, "_last_stale_alert_ts", 0.0) monkeypatch.setattr(ad, "_market_open_or_true_v2", lambda: True) alerts = [] monkeypatch.setattr(ad, "_spawn_bg_v2", lambda coro: (alerts.append(1), coro.close())) # 텔레그램 실호출 방지: 코루틴은 만들되 실행하지 않는다(_spawn_bg_v2 목이 close) import app.services.telegram_notifier as tn async def _boom(*a, **k): raise AssertionError("telegram must not run") monkeypatch.setattr(tn.TelegramNotifier, "notify_system_warning", _boom) return alerts def _stale(monkeypatch): monkeypatch.setattr(ad, "_last_success_ts", time.monotonic() - ad._STALE_LIMIT_SEC - 1) def test_fresh_keeps_mode(monkeypatch, _state): monkeypatch.setattr(ad, "_last_success_ts", time.monotonic()) assert ad.enforce_staleness_v2().trading_mode == TradingMode.NORMAL.value assert not _state def test_stale_in_market_degrades_and_alerts(monkeypatch, _state): _stale(monkeypatch) p = ad.enforce_staleness_v2() assert p.trading_mode == TradingMode.DEFENSIVE.value assert p.fallback_used is False assert _state == [1] ad.enforce_staleness_v2() # 경보 간격 내 재호출은 중복 경보 없음 assert _state == [1] def test_stale_outside_market_keeps(monkeypatch, _state): monkeypatch.setattr(ad, "_market_open_or_true_v2", lambda: False) _stale(monkeypatch) assert ad.enforce_staleness_v2().trading_mode == TradingMode.NORMAL.value def test_crisis_not_loosened(monkeypatch, _state): ad.force_mode_override("crisis", "t") _stale(monkeypatch) assert ad.enforce_staleness_v2().trading_mode == TradingMode.CRISIS.value @pytest.mark.asyncio async def test_refresh_chart_failure_degrades(monkeypatch, _state): """차트 조회 전부 실패(데이터 부족) 경로에서도 staleness 강등이 적용된다.""" import app.services.naver_finance as nf import app.services.korea_investment_api as kia async def _naver(*a, **k): raise RuntimeError("naver down") class _Api: async def get_daily_chart(self, *a, **k): raise RuntimeError("kis down") monkeypatch.setattr(nf, "get_naver_daily_chart", _naver) monkeypatch.setattr(kia, "real_market_api", _Api()) _stale(monkeypatch) p = await ad.refresh_adaptive_params() assert p.trading_mode == TradingMode.DEFENSIVE.value def test_get_current_ttl_expired_degrades(monkeypatch, _state): monkeypatch.setattr(ad, "_last_update_ts", time.monotonic() - ad._CACHE_TTL - 5) _stale(monkeypatch) assert ad.get_current_adaptive_params().trading_mode == TradingMode.DEFENSIVE.value