安全加固 + 数据管线修复 + 质量改进(第2批)
安全加固: - /api/v1/predict 限流改用 get_client_ip 防 X-Forwarded-For 伪造 - 新增 TRUST_PROXY_HEADERS/REQUIRE_ADMIN_AUTH 配置,默认 fail-closed - 生产环境管理接口未配置鉴权时拒绝(503),不再放行 数据管线修复: - injuries IntegrityError 改用 begin_nested(SAVEPOINT)隔离批次 - injuries 空名单 vs 未配置语义区分(has_data 精确标记) - bzzoiro 统计字段映射(shots/possession/cards) + 入库条件放宽 - available_at 语义收紧(回测防泄漏) - team_names NFKD 去变音双重查找 + 补齐变体键 预测系统改进: - multi-agent 全专家失败时 status=degraded 跳过终裁 - agent_weights 独立持久化到 predictions 表 新增迁移: - 0014_predictions_agent_weights.py 新增测试(8个文件): - test_ip_spoofing.py: 限流防伪造 - test_require_admin_fail_closed.py: 生产 fail-closed - test_injuries_integrity_rollback.py: SAVEPOINT 隔离 - test_injuries_slice_semantic.py: 空名单 vs 未配置 - test_available_at.py: 回测防泄漏 - test_bzzoirot_stats.py: 统计字段映射 - test_team_names_normalize.py: NFKD 变体 - test_multi_agent_degraded.py: 全失败 degraded - test_agent_weights_persist.py: 权重持久化
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""回归测试: 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"
|
||||
Reference in New Issue
Block a user