Files
Profeto/tests/test_p0_home_away.py
T
shangfangjian 44816794d3 refactor: context_builder 按 slice 拆到 src/llm/slices/ 包
单文件拆分(仅搬迁无逻辑修改):
- common.py    共享类型/头信息/_outcome/_is_stats_available
- form.py      form_slice + _get_form
- h2h.py       h2h_slice + _get_h2h
- stats.py     stats_slice(复用 form._get_form)
- home_away.py home_away_slice + _get_home_away
- standings.py standings_slice
- aggregate.py build_context

context_builder.py 改为纯 re-export 门面,公开签名不变。
同步修复测试 patch 目标(p0_home_away/h2h_perspective/multi_agent_cutoff)
与 regressions 源码断言(读 slices/*.py)。

全量测试 270 通过。
2026-09-22 01:33:14 +08:00

192 lines
6.8 KiB
Python

"""回归测试: 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}"