"""回归测试: P0-1 — form_slice / stats_slice 主客身份反转。 用 mock Match 对象验证:当某队在历史比赛中是「客队」时, form_slice 必须正确识别该队当时是客场,赛果应为 L(输), 对手名字和进球数不能反转。 """ from __future__ import annotations from unittest.mock import MagicMock import pytest from src.llm.context_builder import MatchHeader, SliceResult, form_slice, stats_slice def _make_team(team_id: int, name: str) -> MagicMock: t = MagicMock() t.id = team_id t.name = name t.name_zh = None return t def _make_stats( home_xg=1.5, away_xg=1.0, home_shots=12, away_shots=8, home_sot=4, away_sot=3, home_poss=55.0, available_at=None, ) -> MagicMock: s = MagicMock() s.home_xg = home_xg s.away_xg = away_xg s.home_shots = home_shots s.away_shots = away_shots s.home_shots_on_target = home_sot s.away_shots_on_target = away_sot s.home_possession = home_poss s.available_at = available_at return s def _make_match( match_id: int, home_team_id: int, away_team_id: int, home_goals: int, away_goals: int, home_name: str = "H", away_name: str = "A", stats=None, ) -> MagicMock: m = MagicMock() m.id = match_id m.home_team_id = home_team_id m.away_team_id = away_team_id m.home_goals = home_goals m.away_goals = away_goals m.stats = stats m.home_team = _make_team(home_team_id, home_name) m.away_team = _make_team(away_team_id, away_name) return m def _make_header(home_id=1, away_id=2, home_name="利物浦", away_name="阿森纳") -> MatchHeader: return MatchHeader( match_id=999, home_name=home_name, away_name=away_name, league_name="英超", season="2025-2026", match_date="2026-01-15 20:00 UTC", match_dt=None, stage=None, home_team_id=home_id, away_team_id=away_id, league_id=1, ) class TestFormSliceHomeAwayIdentity: """P0-1: form_slice 必须根据每场历史比赛的真实主客来判断赛果。""" @pytest.mark.asyncio async def test_home_team_away_loss_shows_L(self): """ 场景: 本场利物浦是主队(home_id=1),历史上一场它作为客队 1-3 输给曼城。 正确输出: L 3-1 vs 曼城 (赛果为输,对手为曼城) 原bug: W 3-1 vs 曼城 (把客场输球算成主场赢球) """ header = _make_header(home_id=1, away_id=2, home_name="利物浦", away_name="阿森纳") hist_match = _make_match( match_id=100, home_team_id=5, # 曼城主场 away_team_id=1, # 利物浦客场 home_goals=3, away_goals=1, home_name="曼城", away_name="利物浦", ) import src.llm.slices.form as cb orig_get_form = cb._get_form async def mock_get_form(db, team_id, before, *, limit): return [hist_match] if team_id == 1 else [] cb._get_form = mock_get_form try: result = await form_slice(header, limit=5, before=None, db=MagicMock()) finally: cb._get_form = orig_get_form text = str(result) assert "L 3-1 vs 曼城" in text, f"期望「L 3-1 vs 曼城」,实际输出:\n{text}" assert "W 3-1" not in text, f"不应出现 W 3-1(客场输球不能算主场赢):\n{text}" @pytest.mark.asyncio async def test_away_team_home_win_shows_W_for_that_team(self): """ 场景: 本场阿森纳是客队(away_id=2),历史上一场它作为主队 2-0 赢了切尔西。 从阿森纳视角: is_home=True → W 2-0 vs 切尔西。 原bug: side 固定为 "away" → _outcome(2,0,"away") = L → 输出 L 2-0 vs 切尔西(反转!) """ header = _make_header(home_id=1, away_id=2, home_name="利物浦", away_name="阿森纳") hist_match = _make_match( match_id=101, home_team_id=2, # 阿森纳主场 away_team_id=4, # 切尔西客场 home_goals=2, away_goals=0, home_name="阿森纳", away_name="切尔西", ) import src.llm.slices.form as cb orig_get_form = cb._get_form async def mock_get_form(db, team_id, before, *, limit): return [hist_match] if team_id == 2 else [] cb._get_form = mock_get_form try: result = await form_slice(header, limit=5, before=None, db=MagicMock()) finally: cb._get_form = orig_get_form text = str(result) assert "W 2-0 vs 切尔西" in text, f"期望「W 2-0 vs 切尔西」,实际输出:\n{text}" assert "L 2-0 vs 切尔西" not in text, f"不应出现 L 2-0(主场赢球不能算客场输):\n{text}" class TestStatsSliceHomeAwayIdentity: """P0-1: stats_slice 进球/失球/xG 必须按历史比赛真实主客取值。""" @pytest.mark.asyncio async def test_home_team_away_match_goals_not_swapped(self): """ 场景: 本场利物浦是主队,历史上一场它作为客队 1-3 输给曼城(xG 0.8 vs 2.5)。 从利物浦视角: 进球=1(away_goals), 失球=3(home_goals), xG=0.8(away_xg)。 原bug: side="home" → 进球=3, 失球=1, xG=2.5 —— 全部反了! """ header = _make_header(home_id=1, away_id=2, home_name="利物浦", away_name="阿森纳") hist_match = _make_match( match_id=200, home_team_id=5, # 曼城主场 away_team_id=1, # 利物浦客场 home_goals=3, away_goals=1, home_name="曼城", away_name="利物浦", stats=_make_stats(home_xg=2.5, away_xg=0.8, home_shots=15, away_shots=5, home_sot=6, away_sot=2, home_poss=60.0), ) import src.llm.slices.stats as cb orig_get_form = cb._get_form async def mock_get_form(db, team_id, before, *, limit): return [hist_match] if team_id == 1 else [] cb._get_form = mock_get_form try: result = await stats_slice(header, limit=10, before=None, db=MagicMock()) finally: cb._get_form = orig_get_form text = str(result) # 利物浦客场 1-3 输: 进球 1, 失球 3 assert "场均进球 1.00" in text, f"期望场均进球 1.00,实际输出:\n{text}" assert "场均失球 3.00" in text, f"期望场均失球 3.00,实际输出:\n{text}" # 原bug: 进球 3, 失球 1 (反了) assert "场均进球 3.00" not in text, f"不应出现场均进球 3.00(反转):\n{text}" # xG: 利物浦 away_xg=0.8 assert "场均 xG 0.80" in text, f"期望场均 xG 0.80,实际输出:\n{text}" # shots: 利物浦 away_shots=5 assert "场均射门 5.0" in text, f"期望场均射门 5.0,实际输出:\n{text}"