按「测试腐化(a)/ 源码缺陷(b)/ 测试污染(c)」逐项定性,仅改 tests/: - 外机绝对路径(4): 迁移文件路径改为相对仓库根解析(沿用 test_regressions._read 约定),断言内容保持不变。 - cutoff 用例(4+1): mock 未生效的真因是 _agent_provider 被换成同步 lambda,await 抛 TypeError 被生产代码吞掉后 IndexError;改为 async mock 并按 run_specialists/load_match_header/_agent_provider 的真实契约 打补丁。degraded 用例再加 _upsert_prediction 顶层 kwargs(model)采集。 - 陈旧断言(3): agent 键按现契约断言中文映射;h2h mock 改 async; Match.stats 按设计为 lazy="select",从 MATCH_RELATIONS 移出并单独 固化该设计决定。 - P0-3 守卫(1): seg 越界扫到下游 stats 管线导致误报,改为按缩进收口; 合法形状含经 raw 派生变量中转的写法,并补元测试确保守卫仍能抓到回归。 - 交叉污染(6): test_multi_agent_cutoff 用 patch.object 精确还原,消除 裸赋值泄漏的同步 mock;现已验证顺序无关。
137 lines
5.5 KiB
Python
137 lines
5.5 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.context_builder 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.context_builder 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.context_builder 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
|