安全加固: - /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: 权重持久化
111 lines
4.1 KiB
Python
111 lines
4.1 KiB
Python
"""回归测试: 伤停切片区分「查询成功但无人伤停」与「无数据/未接入」。
|
|
|
|
验证:
|
|
1. 查询成功 + 空结果 → has_data=True
|
|
2. 源未配置 → has_data=False
|
|
3. 查询异常 → has_data=False
|
|
4. 查询成功 + 有数据 → has_data=True
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.data.injuries import InjuryQueryResult, get_injuries_for_match
|
|
from src.llm.context_builder import MatchHeader, injuries_slice
|
|
|
|
|
|
def _make_header():
|
|
return MatchHeader(
|
|
match_id=999, home_name="A", away_name="B",
|
|
league_name="X", season=None, match_date="?",
|
|
match_date=None, stage=None,
|
|
home_team_id=1, away_team_id=2, league_id=1,
|
|
)
|
|
|
|
|
|
class TestInjuryQueryResult:
|
|
"""InjuryQueryResult 基础属性。"""
|
|
|
|
def test_has_data_success(self):
|
|
result = InjuryQueryResult(records=[], query_status="success")
|
|
assert result.has_data is True
|
|
|
|
def test_has_data_source_not_configured(self):
|
|
result = InjuryQueryResult(records=[], query_status="source_not_configured")
|
|
assert result.has_data is False
|
|
|
|
def test_has_data_query_error(self):
|
|
result = InjuryQueryResult(records=[], query_status="query_error")
|
|
assert result.has_data is False
|
|
|
|
|
|
class TestInjuriesSliceEmptyVsNotConfigured:
|
|
"""injuries_slice 应区分「查询成功但为空」与「无数据/未接入」。"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_result_has_data_true(self):
|
|
"""查询成功 + 空结果 → has_data=True,文案显示「当前无伤停记录」。"""
|
|
header = _make_header()
|
|
|
|
# Mock get_injuries_for_match 返回成功但空的结果
|
|
async def mock_query(db, team_id, match_date, as_of=None):
|
|
return InjuryQueryResult(records=[], query_status="success")
|
|
|
|
with patch("src.llm.context_builder.get_injuries_for_match", mock_query):
|
|
result = await injuries_slice(header, before=None)
|
|
|
|
assert result.has_data is True, "查询成功+空结果应 has_data=True"
|
|
assert "当前无伤停记录" in result.text, "文案应表明无伤停"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_source_not_configured_has_data_false(self):
|
|
"""源未配置 → has_data=False。"""
|
|
header = _make_header()
|
|
|
|
async def mock_query(db, team_id, match_date, as_of=None):
|
|
return InjuryQueryResult(records=[], query_status="source_not_configured")
|
|
|
|
with patch("src.llm.context_builder.get_injuries_for_match", mock_query):
|
|
result = await injuries_slice(header, before=None)
|
|
|
|
assert result.has_data is False, "源未配置应 has_data=False"
|
|
assert "伤停源未配置" in result.text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_error_has_data_false(self):
|
|
"""查询异常 → has_data=False。"""
|
|
header = _make_header()
|
|
|
|
async def mock_query(db, team_id, match_date, as_of=None):
|
|
return InjuryQueryResult(records=[], query_status="query_error")
|
|
|
|
with patch("src.llm.context_builder.get_injuries_for_match", mock_query):
|
|
result = await injuries_slice(header, before=None)
|
|
|
|
assert result.has_data is False, "查询异常应 has_data=False"
|
|
assert "查询异常" in result.text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_with_records_has_data_true(self):
|
|
"""查询成功 + 有数据 → has_data=True。"""
|
|
header = _make_header()
|
|
|
|
mock_inj = MagicMock()
|
|
mock_inj.reason = "Hamstring"
|
|
mock_inj.injury_type = None
|
|
mock_inj.player_name = "Player A"
|
|
|
|
async def mock_query(db, team_id, match_date, as_of=None):
|
|
if team_id == 1:
|
|
return InjuryQueryResult(records=[mock_inj], query_status="success")
|
|
return InjuryQueryResult(records=[], query_status="success")
|
|
|
|
with patch("src.llm.context_builder.get_injuries_for_match", mock_query):
|
|
result = await injuries_slice(header, before=None)
|
|
|
|
assert result.has_data is True
|
|
assert "Player A" in result.text
|