"""回归测试: match_stats.available_at 回测防泄漏语义。 验证: 1. _is_stats_available: available_at is None + cutoff 不为 None → 不可用 2. _is_stats_available: available_at is None + cutoff is None(实盘) → 可用(兼容旧数据) 3. _is_stats_available: available_at > cutoff → 不可用 4. _is_stats_available: available_at <= cutoff → 可用 """ from __future__ import annotations from datetime import datetime, timezone, timedelta from unittest.mock import MagicMock import pytest from src.llm.context_builder import _is_stats_available def _make_stats(available_at): s = MagicMock() s.available_at = available_at return s class TestIsStatsAvailable: """_is_stats_available 回测防泄漏语义。""" def test_none_before_allows_none_available_at(self): """实盘模式(before=None): available_at 为 None 时允许(兼容旧数据)。""" stats = _make_stats(available_at=None) assert _is_stats_available(stats, before=None) is True def test_cutoff_with_none_available_at_is_unavailable(self): """回测模式(before=cutoff): available_at 为 None → 不可用(保守)。 这是核心修复:防止无血缘时间的后期回填数据进入回测。 """ stats = _make_stats(available_at=None) cutoff = datetime(2026, 1, 13, 20, 0, tzinfo=timezone.utc) assert _is_stats_available(stats, before=cutoff) is False def test_available_at_after_cutoff_is_unavailable(self): """available_at 在 cutoff 之后 → 不可用。""" cutoff = datetime(2026, 1, 13, 20, 0, tzinfo=timezone.utc) stats = _make_stats(available_at=cutoff + timedelta(hours=1)) assert _is_stats_available(stats, before=cutoff) is False def test_available_at_before_cutoff_is_available(self): """available_at 在 cutoff 之前 → 可用。""" cutoff = datetime(2026, 1, 13, 20, 0, tzinfo=timezone.utc) stats = _make_stats(available_at=cutoff - timedelta(hours=1)) assert _is_stats_available(stats, before=cutoff) is True def test_available_at_equals_cutoff_is_available(self): """available_at == cutoff → 可用(边界包含)。""" cutoff = datetime(2026, 1, 13, 20, 0, tzinfo=timezone.utc) stats = _make_stats(available_at=cutoff) assert _is_stats_available(stats, before=cutoff) is True def test_real_world_scenario_backtest_avoids_future_data(self): """真实场景:回测时,赛后才生成的统计数据不应出现。 比赛:2026-01-15 20:00 cutoff(回测):2026-01-14 20:00(赛前 1 天) stats 在赛后才生成(available_at=2026-01-15 22:00) → 不可用 """ cutoff = datetime(2026, 1, 14, 20, 0, tzinfo=timezone.utc) stats = _make_stats(available_at=datetime(2026, 1, 15, 22, 0, tzinfo=timezone.utc)) assert _is_stats_available(stats, before=cutoff) is False class TestBzzoirotAvailableAt: """验证 bzzoiro.py 写入 available_at 使用 match_date 而非 now。""" def test_bzzoiro_sets_available_at_from_match_date(self): """bzzoiro.py 应在创建 stats 时使用 nm.date 作为 available_at。""" import inspect from src.data import bzzoiro source = inspect.getsource(bzzoiro) # 验证:存在 available_at = nm.date 的逻辑 assert 'available_at = nm.date if nm.date else now' in source, \ "bzzoiro.py 应使用 nm.date 作为 available_at" def test_bzzoiro_existing_match_uses_match_date(self): """bzzoiro.py 更新已有比赛时也应用 nm.date。""" import inspect from src.data import bzzoiro source = inspect.getsource(bzzoiro) # 验证两处都更新 count = source.count('available_at = nm.date if nm.date else now') assert count == 2, f"期望 2 处使用 nm.date,实际 {count} 处" class TestUnderstatAvailableAt: """验证 understat.py 写入 available_at 使用 match_date。""" def test_understat_sets_available_at_from_match_date(self): """understat.py 应使用 existing.match_date 作为 available_at。""" import inspect from src.data import understat source = inspect.getsource(understat) assert 'available_at = match_date' in source, \ "understat.py 应使用 match_date 作为 available_at"