"""[하드닝 R8] 추천 경로 매도의 ActiveTrade 대상 역추적. 역추적 SELECT 예외 시 로그 없이 'swing' 이름으로 폴백하면, 실제 행이 'synced' 일 때 close/upsert 매칭이 0건 → 유령 active 행 + remaining_pct 미갱신(분할 단계 재발동). 예외 시에는 warning 을 남기고 비-DT 최신 행 필터(strategy_name_not)로 겨냥해야 한다. """ import logging from types import SimpleNamespace import pytest from app.services import trade_executor as te from app.services.db_worker import DT_STRATEGY_NAMES class _DBRaises: async def execute(self, *_a, **_k): raise RuntimeError("db down") class _DBReturns: def __init__(self, row): self._row = row async def execute(self, *_a, **_k): row = self._row class _R: def scalars(self_inner): return SimpleNamespace(first=lambda: row) return _R() class _DBMustNotBeCalled: async def execute(self, *_a, **_k): raise AssertionError("strategy_name 이 있으면 역추적 SELECT 를 하지 않아야 한다") @pytest.mark.asyncio async def test_lookup_exception_targets_non_dt_row_and_warns(caplog): caplog.set_level(logging.WARNING, logger=te.logger.name) name, filt = await te._resolve_sell_active_trade_target_v2( _DBRaises(), {"strategy_type": "swing"}, "323410" ) assert filt is True, "예외 시 'swing' 이름 폴백 → synced 행 매칭 0건(유령 행)" assert name is None assert any("역추적 실패" in r.getMessage() and r.levelno >= logging.WARNING for r in caplog.records), "예외 흔적이 warning 이상으로 남아야 한다(§4)" @pytest.mark.asyncio async def test_lookup_exception_keeps_dt_tagged_holding_on_dt_name(): dt = DT_STRATEGY_NAMES[0] name, filt = await te._resolve_sell_active_trade_target_v2( _DBRaises(), {"strategy_type": dt}, "005930" ) assert (name, filt) == (dt, False) @pytest.mark.asyncio async def test_lookup_finds_synced_row_name(): name, filt = await te._resolve_sell_active_trade_target_v2( _DBReturns(SimpleNamespace(strategy_name="synced")), {}, "005930" ) assert (name, filt) == ("synced", False) @pytest.mark.asyncio async def test_lookup_null_name_row_uses_non_dt_filter(): name, filt = await te._resolve_sell_active_trade_target_v2( _DBReturns(SimpleNamespace(strategy_name=None)), {"strategy_type": "swing"}, "005930" ) assert (name, filt) == (None, True) @pytest.mark.asyncio async def test_explicit_strategy_name_skips_lookup(): name, filt = await te._resolve_sell_active_trade_target_v2( _DBMustNotBeCalled(), {"strategy_name": "MOMENTUM_BREAKOUT"}, "005930" ) assert (name, filt) == ("MOMENTUM_BREAKOUT", False) def test_execute_sell_uses_resolver(): import inspect src = inspect.getsource(te._execute_sell) assert "_resolve_sell_active_trade_target_v2(" in src assert "_at_filter_not_dt = False\n" not in src.split("_resolve_sell_active_trade_target_v2(")[1][:400]