import pytest from app.services.mdd_manager import MDDManager from app.services import mdd_state_backup as B class _RedisDown(MDDManager): async def load_from_redis(self): raise AssertionError("unused") @pytest.mark.asyncio async def test_file_fallback_preserves_peak(tmp_path, monkeypatch): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 10_000_000.0 a.is_paused = True a.reduction_applied = "10pct" a._reduction_executed_10pct = True assert await B.save_mdd_backup_v2(a, p) b = MDDManager() async def _no_redis(): return False monkeypatch.setattr(b, "load_from_redis", _no_redis) assert await B.restore_mdd_state_v2(b, p) == "file" assert b.peak_balance == 10_000_000.0 and b.is_paused assert b.reduction_applied == "10pct" and b._reduction_executed_10pct # 복원된 peak 로 낙폭 계산 (재시드되지 않음) r = await b.update(9_500_000.0) assert round(r["drawdown"], 1) == -5.0 @pytest.mark.asyncio async def test_none_when_no_sources(tmp_path, monkeypatch): b = MDDManager() async def _no_redis(): return False monkeypatch.setattr(b, "load_from_redis", _no_redis) assert await B.restore_mdd_state_v2(b, tmp_path / "x.json") == "none" @pytest.mark.asyncio async def test_unseeded_does_not_overwrite_backup(tmp_path): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 5.0 await B.save_mdd_backup_v2(a, p) assert not await B.save_mdd_backup_v2(MDDManager(), p) assert '"peak_balance":5' in p.read_text().replace(" ", "") @pytest.mark.asyncio async def test_corrupt_file_returns_none(tmp_path, monkeypatch): p = tmp_path / "m.json" p.write_text("{bad") b = MDDManager() assert not await B.restore_mdd_from_backup_v2(b, p) def _redis_loader(mgr, peak, **flags): async def _load(): mgr.peak_balance = peak for k, v in flags.items(): setattr(mgr, k, v) return True return _load @pytest.mark.asyncio async def test_stale_redis_loses_to_newer_file(tmp_path, monkeypatch): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 12_000_000.0 a._reduction_executed_10pct = True assert await B.save_mdd_backup_v2(a, p) b = MDDManager() monkeypatch.setattr(b, "load_from_redis", _redis_loader(b, 10_000_000.0)) assert await B.restore_mdd_state_v2(b, p) == "file" assert b.peak_balance == 12_000_000.0 and b._reduction_executed_10pct @pytest.mark.asyncio async def test_equal_peak_prefers_more_conservative_flags(tmp_path, monkeypatch): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 10_000_000.0 a._reduction_executed_10pct = True a.is_paused = True await B.save_mdd_backup_v2(a, p) b = MDDManager() monkeypatch.setattr(b, "load_from_redis", _redis_loader(b, 10_000_000.0)) assert await B.restore_mdd_state_v2(b, p) == "file" assert b.is_paused and b._reduction_executed_10pct @pytest.mark.asyncio async def test_redis_wins_when_newer_or_file_missing(tmp_path, monkeypatch): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 9_000_000.0 await B.save_mdd_backup_v2(a, p) b = MDDManager() monkeypatch.setattr(b, "load_from_redis", _redis_loader(b, 10_000_000.0)) assert await B.restore_mdd_state_v2(b, p) == "redis" assert b.peak_balance == 10_000_000.0 c = MDDManager() monkeypatch.setattr(c, "load_from_redis", _redis_loader(c, 10_000_000.0)) assert await B.restore_mdd_state_v2(c, tmp_path / "none.json") == "redis" @pytest.mark.asyncio async def test_rebased_lower_peak_in_redis_wins_by_saved_at(tmp_path, monkeypatch): """reset 재기준으로 낮아진 Redis peak 를 옛(높은 peak) 파일이 되살리면 안 된다.""" p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 10_000_000.0 a.is_paused = True await B.save_mdd_backup_v2(a, p) import json, time file_at = json.loads(p.read_text())["saved_at"] b = MDDManager() monkeypatch.setattr(b, "load_from_redis", _redis_loader(b, 8_000_000.0)) async def _r_at(): return file_at + 5.0 monkeypatch.setattr(B, "_read_redis_saved_at_v2", _r_at) assert await B.restore_mdd_state_v2(b, p) == "redis" assert b.peak_balance == 8_000_000.0 and not b.is_paused @pytest.mark.asyncio async def test_newer_file_wins_by_saved_at_even_if_lower_peak(tmp_path, monkeypatch): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 7_000_000.0 await B.save_mdd_backup_v2(a, p) import json file_at = json.loads(p.read_text())["saved_at"] b = MDDManager() monkeypatch.setattr(b, "load_from_redis", _redis_loader(b, 10_000_000.0)) async def _r_at(): return file_at - 100.0 monkeypatch.setattr(B, "_read_redis_saved_at_v2", _r_at) assert await B.restore_mdd_state_v2(b, p) == "file" assert b.peak_balance == 7_000_000.0 @pytest.mark.asyncio async def test_save_everywhere_stamps_same_saved_at(tmp_path, monkeypatch): p = tmp_path / "m.json" a = MDDManager() a.peak_balance = 5_000_000.0 seen = {} async def _w(key, state): seen.update(state) monkeypatch.setattr(a, "_write_state_v2", _w) assert await B.save_mdd_state_everywhere_v2(a, p) import json assert seen["saved_at"] == json.loads(p.read_text())["saved_at"] assert not await B.save_mdd_state_everywhere_v2(MDDManager(), p)