P0-05: confidence → subjective_confidence 改名(15 文件)
LLM 主观置信度与概率分离
P0-02: predictions 增加 cutoff_at + input_hash(轻量快照)
MatchContext 暴露 match_dt
单/多 Agent 路径均记录快照元数据
P2-05: 数据库 CHECK 约束
- pred_home_goals >= 0
- pred_away_goals >= 0
- subjective_confidence 0~1
- pred_1x2 IN (1,X,2)
- mode IN (single,multi)
P2-06: limit 分页约束(ge=1, le=200)
P2-01: 新增 /health/ready 就绪检查
113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
"""测试核心路径。"""
|
|
import pytest
|
|
from datetime import datetime, timezone
|
|
from src.data.normalize import NormalizedMatch, normalize_bzzoiro, _parse_date, _to_int, _to_float, derive_season_label
|
|
|
|
|
|
class TestNormalize:
|
|
def test_parse_date_iso(self):
|
|
assert _parse_date("2026-09-15T15:00:00Z") is not None
|
|
|
|
def test_parse_date_bare(self):
|
|
assert _parse_date("2026-09-15") is not None
|
|
|
|
def test_parse_date_none(self):
|
|
assert _parse_date(None) is None
|
|
assert _parse_date("") is None
|
|
|
|
def test_to_int_strict(self):
|
|
assert _to_int("2") == 2
|
|
assert _to_int("2.0") == 2
|
|
assert _to_int("2.8") is None # 拒绝非整数值
|
|
assert _to_int(None) is None
|
|
assert _to_int("-") is None
|
|
|
|
def test_to_float(self):
|
|
assert _to_float("1.5") == 1.5
|
|
assert _to_float(None) is None
|
|
|
|
def test_normalize_bzzoiro_finished(self):
|
|
raw = {
|
|
"event_date": "2026-09-15T15:00:00Z",
|
|
"status": "finished",
|
|
"home_team": "Man City",
|
|
"away_team": "Man United",
|
|
"home_score": 2,
|
|
"away_score": 1,
|
|
"round_number": 5,
|
|
}
|
|
m = normalize_bzzoiro(raw, "E0")
|
|
assert m is not None
|
|
assert m.home_team == "Manchester City"
|
|
assert m.away_team == "Manchester United"
|
|
assert m.home_goals == 2
|
|
assert m.away_goals == 1
|
|
assert m.match_status == "finished"
|
|
assert m.match_stage == "第 5 轮"
|
|
|
|
def test_normalize_bzzoiro_unknown_status(self):
|
|
raw = {"event_date": "2026-09-15", "status": "weird", "home_team": "A", "away_team": "B"}
|
|
assert normalize_bzzoiro(raw, "E0") is None
|
|
|
|
def test_normalized_match_validate_finished_no_score(self):
|
|
m = NormalizedMatch(
|
|
league_type="E0", date=_parse_date("2026-09-15"),
|
|
home_team="A", away_team="B", match_status="finished",
|
|
)
|
|
with pytest.raises(ValueError, match="must have score"):
|
|
m.validate()
|
|
|
|
def test_normalized_match_validate_goals_range(self):
|
|
m = NormalizedMatch(
|
|
league_type="E0", date=_parse_date("2026-09-15"),
|
|
home_team="A", away_team="B", match_status="finished",
|
|
home_goals=50, away_goals=0,
|
|
)
|
|
with pytest.raises(ValueError, match="out of range"):
|
|
m.validate()
|
|
|
|
|
|
class TestSeasonLabel:
|
|
def test_august_is_new_season(self):
|
|
# 8 月属于新赛季
|
|
d = datetime(2026, 8, 15, tzinfo=timezone.utc)
|
|
assert derive_season_label(d) == "2026-2027"
|
|
|
|
def test_july_is_old_season(self):
|
|
# 7 月属于上一赛季
|
|
d = datetime(2026, 7, 15, tzinfo=timezone.utc)
|
|
assert derive_season_label(d) == "2025-2026"
|
|
|
|
def test_january_is_old_season(self):
|
|
d = datetime(2026, 1, 15, tzinfo=timezone.utc)
|
|
assert derive_season_label(d) == "2025-2026"
|
|
|
|
|
|
class TestProviderMock:
|
|
"""用 mock 测试 LLM provider 的解析逻辑。"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_provider_parses_json(self, monkeypatch):
|
|
from src.llm.provider import LLMProvider
|
|
|
|
async def fake_post(*args, **kwargs):
|
|
class FakeResp:
|
|
status_code = 200
|
|
def raise_for_status(self): pass
|
|
def json(self):
|
|
return {
|
|
"choices": [{"message": {"content": '{"pred_home_goals": 1.5, "pred_away_goals": 1.0, "1x2": "1", "subjective_confidence": 0.7, "reasoning": "test"}'}}],
|
|
"usage": {"prompt_tokens": 100, "completion_tokens": 50},
|
|
}
|
|
return FakeResp()
|
|
|
|
import httpx
|
|
monkeypatch.setattr(httpx.AsyncClient, "post", fake_post)
|
|
|
|
p = LLMProvider(api_key="test", model="gpt-4o")
|
|
resp = await p.chat("sys", "user", json_mode=True)
|
|
assert resp.error is None
|
|
assert resp.parsed is not None
|
|
assert resp.parsed["pred_home_goals"] == 1.5
|
|
assert resp.parsed["1x2"] == "1"
|