单文件拆分(仅搬迁无逻辑修改): - 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 通过。
137 lines
5.4 KiB
Python
137 lines
5.4 KiB
Python
"""回归测试: H2H 切片「主队 n 胜」统计视角修复。
|
|
|
|
验证: 历史交锋汇总必须从「当前主队」视角统计胜/平/负,
|
|
而非按「场地主队」统计。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.llm.context_builder import MatchHeader, h2h_slice
|
|
|
|
|
|
def _make_team(tid: int, name: str) -> MagicMock:
|
|
t = MagicMock()
|
|
t.id = tid
|
|
t.name = name
|
|
return t
|
|
|
|
|
|
def _make_h2h_match(mid, home_id, away_id, home_goals, away_goals, home_name="H", away_name="A"):
|
|
m = MagicMock()
|
|
m.id = mid
|
|
m.home_team_id = home_id
|
|
m.away_team_id = away_id
|
|
m.home_goals = home_goals
|
|
m.away_goals = away_goals
|
|
m.match_date = None
|
|
m.home_team = _make_team(home_id, home_name)
|
|
m.away_team = _make_team(away_id, away_name)
|
|
return m
|
|
|
|
|
|
def _make_header(home_id=1, away_id=2, home_name="利物浦", away_name="阿森纳"):
|
|
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 TestH2HCurrentHomePerspective:
|
|
"""H2H 汇总统计必须从当前主队视角出发。"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_swapped_home_away_perspective(self):
|
|
"""
|
|
场景: 当前比赛利物浦(home_id=1) vs 阿森纳(away_id=2)。
|
|
历史交锋两场:
|
|
1. 利物浦主场 2-0 阿森纳 (home_id=1, away_id=2)
|
|
2. 阿森纳主场 3-1 利物浦 (home_id=2, away_id=1)
|
|
|
|
从利物浦视角: 1胜(2-0) 1负(1-3)。
|
|
原bug: 按场地主队统计 → "主队 1胜 0平 1负"(第二场场地主队是阿森纳,赢了),
|
|
导致「利物浦横扫」的假象。
|
|
"""
|
|
header = _make_header(home_id=1, away_id=2, home_name="利物浦", away_name="阿森纳")
|
|
matches = [
|
|
_make_h2h_match(100, home_id=1, away_id=2, home_goals=2, away_goals=0,
|
|
home_name="利物浦", away_name="阿森纳"),
|
|
_make_h2h_match(101, home_id=2, away_id=1, home_goals=3, away_goals=1,
|
|
home_name="阿森纳", away_name="利物浦"),
|
|
]
|
|
import src.llm.slices.h2h as cb
|
|
orig = cb._get_h2h
|
|
async def mock_get_h2h(db, home_id, away_id, before, *, limit):
|
|
return matches
|
|
cb._get_h2h = mock_get_h2h
|
|
try:
|
|
result = await h2h_slice(header, limit=8, before=None)
|
|
text = str(result)
|
|
print(text)
|
|
# 从利物浦视角: 1胜 0平 1负
|
|
assert "1胜 0平 1负" in text, f"期望「1胜 0平 1负」,实际:\n{text}"
|
|
assert "利物浦" in text, f"应标明当前主队视角:\n{text}"
|
|
# 原bug输出: "主队 1胜 0平 1负"(模糊的「主队」,实际是场地主队)
|
|
# 修复后: "从当前主队 利物浦 视角: 1胜 0平 1负"
|
|
assert "从当前主队" in text, f"应标明「从当前主队」视角:\n{text}"
|
|
finally:
|
|
cb._get_h2h = orig
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_all_home_wins_from_current_perspective(self):
|
|
"""
|
|
当前主队所有交锋都是主场且全胜 → 全部计为当前主队胜。
|
|
"""
|
|
header = _make_header(home_id=1, away_id=2, home_name="曼城", away_name="诺维奇")
|
|
matches = [
|
|
_make_h2h_match(200, home_id=1, away_id=2, home_goals=3, away_goals=0,
|
|
home_name="曼城", away_name="诺维奇"),
|
|
_make_h2h_match(201, home_id=1, away_id=2, home_goals=2, away_goals=1,
|
|
home_name="曼城", away_name="诺维奇"),
|
|
]
|
|
import src.llm.slices.h2h as cb
|
|
|
|
async def mock_get_h2h(db, h, a, before, **kw):
|
|
# 真实契约是 async(见 context_builder.py 的 `h2h = await _get_h2h(...)`),
|
|
# 同步 lambda 会抛 TypeError: object list can't be used in 'await' expression。
|
|
return matches
|
|
|
|
with patch.object(cb, "_get_h2h", mock_get_h2h):
|
|
result = await h2h_slice(header, limit=8, before=None)
|
|
text = str(result)
|
|
assert "2胜 0平 0负" in text, f"期望「2胜 0平 0负」,实际:\n{text}"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_draw_counted_correctly(self):
|
|
"""场景: 两场交锋一胜一平,验证平局也被正确计数。"""
|
|
header = _make_header(home_id=1, away_id=2, home_name="切尔西", away_name="热刺")
|
|
matches = [
|
|
_make_h2h_match(300, home_id=1, away_id=2, home_goals=1, away_goals=1,
|
|
home_name="切尔西", away_name="热刺"), # 平局
|
|
_make_h2h_match(301, home_id=2, away_id=1, home_goals=0, away_goals=2,
|
|
home_name="热刺", away_name="切尔西"), # 切尔西客场 2-0 赢
|
|
]
|
|
import src.llm.slices.h2h as cb
|
|
orig = cb._get_h2h
|
|
async def mock_get_h2h(db, h, a, before, *, limit):
|
|
return matches
|
|
cb._get_h2h = mock_get_h2h
|
|
try:
|
|
result = await h2h_slice(header, limit=8, before=None)
|
|
text = str(result)
|
|
# 切尔西视角: 1胜(客场2-0) 1平(主场1-1) 0负
|
|
assert "1胜 1平 0负" in text, f"期望「1胜 1平 0负」,实际:\n{text}"
|
|
finally:
|
|
cb._get_h2h = orig
|