"""测试核心路径。""" 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", "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"