test: 修复 13 个腐化用例,套件恢复全绿且顺序无关
按「测试腐化(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;现已验证顺序无关。
This commit is contained in:
@@ -35,40 +35,49 @@ class TestMultiAgentCutoffPropagation:
|
||||
async def test_backtest_computes_cutoff_from_match_dt_minus_1_day(self):
|
||||
"""backtest=True → cutoff = match_dt - 1 天,传给所有切片。"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
import src.llm.agents.orchestrator as orch
|
||||
|
||||
match_dt = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
|
||||
header = _make_header(match_dt)
|
||||
|
||||
captured_before = []
|
||||
orig_run_specialists = orch.run_specialists
|
||||
|
||||
async def mock_run_specialists(header, *, version, before=None):
|
||||
captured_before.append(before)
|
||||
return []
|
||||
|
||||
orch.run_specialists = mock_run_specialists
|
||||
orch.load_match_header = lambda mid, db=None: header
|
||||
orch._agent_provider = lambda agent_id, **kw: MagicMock(model="test")
|
||||
async def mock_header(mid, db=None):
|
||||
# load_match_header 是 async,必须是 async 函数;
|
||||
# 且必须走 patch.object(orch 模块属性),因为 predict_match_multi
|
||||
# 通过模块命名空间解析该名字。裸赋值 orch.load_match_header 同样有效,
|
||||
# 但用 patch 可保证退出时精确还原,不向后续测试泄漏。
|
||||
return header
|
||||
|
||||
try:
|
||||
async def mock_provider(agent_id, *, tier, model_override=None):
|
||||
# 真实契约是 async(见 orchestrator._agent_provider),同步 lambda
|
||||
# 会让 `await _agent_provider(...)` 抛 TypeError 并被吞掉。
|
||||
return MagicMock(model="test")
|
||||
|
||||
with patch.object(orch, "run_specialists", mock_run_specialists), \
|
||||
patch.object(orch, "load_match_header", mock_header), \
|
||||
patch.object(orch, "_agent_provider", mock_provider):
|
||||
try:
|
||||
await orch.predict_match_multi(999, backtest=True)
|
||||
except Exception:
|
||||
pass # 后续 aggregator 调用会因 mock 不全而失败,不影响 cutoff 测试
|
||||
|
||||
assert len(captured_before) == 1
|
||||
expected_cutoff = match_dt - timedelta(days=1)
|
||||
assert captured_before[0] == expected_cutoff, (
|
||||
f"backtest cutoff 应为 {expected_cutoff},实际 {captured_before[0]}"
|
||||
)
|
||||
finally:
|
||||
orch.run_specialists = orig_run_specialists
|
||||
assert len(captured_before) == 1
|
||||
expected_cutoff = match_dt - timedelta(days=1)
|
||||
assert captured_before[0] == expected_cutoff, (
|
||||
f"backtest cutoff 应为 {expected_cutoff},实际 {captured_before[0]}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_explicit_cutoff_at_overrides_backtest(self):
|
||||
"""显式 cutoff_at 优先于 backtest 自动计算。"""
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
import src.llm.agents.orchestrator as orch
|
||||
|
||||
match_dt = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
|
||||
@@ -76,97 +85,89 @@ class TestMultiAgentCutoffPropagation:
|
||||
header = _make_header(match_dt)
|
||||
|
||||
captured_before = []
|
||||
orig_run_specialists = orch.run_specialists
|
||||
|
||||
async def mock_run_specialists(header, *, version, before=None):
|
||||
captured_before.append(before)
|
||||
return []
|
||||
|
||||
orch.run_specialists = mock_run_specialists
|
||||
orch.load_match_header = lambda mid, db=None: header
|
||||
orch._agent_provider = lambda agent_id, **kw: MagicMock(model="test")
|
||||
async def mock_header(mid, db=None):
|
||||
return header
|
||||
|
||||
try:
|
||||
async def mock_provider(agent_id, *, tier, model_override=None):
|
||||
return MagicMock(model="test")
|
||||
|
||||
with patch.object(orch, "run_specialists", mock_run_specialists), \
|
||||
patch.object(orch, "load_match_header", mock_header), \
|
||||
patch.object(orch, "_agent_provider", mock_provider):
|
||||
try:
|
||||
await orch.predict_match_multi(999, backtest=True, cutoff_at=explicit_cutoff)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert captured_before[0] == explicit_cutoff
|
||||
finally:
|
||||
orch.run_specialists = orig_run_specialists
|
||||
assert captured_before[0] == explicit_cutoff
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_normal_mode_cutoff_is_match_dt(self):
|
||||
"""非回测模式,无显式 cutoff → cutoff = match_dt。"""
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import patch
|
||||
import src.llm.agents.orchestrator as orch
|
||||
|
||||
match_dt = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
|
||||
header = _make_header(match_dt)
|
||||
|
||||
captured_before = []
|
||||
orig_run_specialists = orch.run_specialists
|
||||
|
||||
async def mock_run_specialists(header, *, version, before=None):
|
||||
captured_before.append(before)
|
||||
return []
|
||||
|
||||
orch.run_specialists = mock_run_specialists
|
||||
orch.load_match_header = lambda mid, db=None: header
|
||||
orch._agent_provider = lambda agent_id, **kw: MagicMock(model="test")
|
||||
async def mock_header(mid, db=None):
|
||||
return header
|
||||
|
||||
try:
|
||||
async def mock_provider(agent_id, *, tier, model_override=None):
|
||||
return MagicMock(model="test")
|
||||
|
||||
with patch.object(orch, "run_specialists", mock_run_specialists), \
|
||||
patch.object(orch, "load_match_header", mock_header), \
|
||||
patch.object(orch, "_agent_provider", mock_provider):
|
||||
try:
|
||||
await orch.predict_match_multi(999, backtest=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert captured_before[0] == match_dt
|
||||
finally:
|
||||
orch.run_specialists = orig_run_specialists
|
||||
assert captured_before[0] == match_dt
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prediction_cutoff_at_stored_not_match_dt(self):
|
||||
"""Prediction 写入时 prediction_cutoff_at = 真正 cutoff,非 match_dt。"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from src.llm.predict import _predict_single, PredictResult
|
||||
from unittest.mock import patch
|
||||
import src.llm.predict as pred
|
||||
|
||||
match_dt = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
|
||||
expected_cutoff = match_dt - timedelta(days=1)
|
||||
|
||||
# Mock build_context to return a context with cutoff
|
||||
import src.llm.predict as pred
|
||||
orig_build = pred.build_context
|
||||
|
||||
class FakeContext:
|
||||
text = "fake"
|
||||
match_dt = match_dt
|
||||
cutoff = expected_cutoff
|
||||
|
||||
async def fake_build(match_id, **kw):
|
||||
FakeContext.match_dt = match_dt
|
||||
|
||||
call_args = {}
|
||||
|
||||
async def tracking_build(match_id, **kw):
|
||||
call_args.update(kw)
|
||||
return FakeContext()
|
||||
|
||||
pred.build_context = fake_build
|
||||
pred._upsert_prediction = lambda session, **kw: MagicMock(id=1, **kw.get("values", {}))
|
||||
|
||||
try:
|
||||
# 此处只验证 cutoff 参数传递,实际 LLM 调用会被 mock 阻断
|
||||
# 重点: build_context 被调用时传入 backtest=True 和正确的 cutoff
|
||||
call_args = {}
|
||||
async def tracking_build(match_id, **kw):
|
||||
call_args.update(kw)
|
||||
return FakeContext()
|
||||
pred.build_context = tracking_build
|
||||
|
||||
with patch.object(pred, "build_context", tracking_build):
|
||||
try:
|
||||
await _predict_single(999, backtest=True)
|
||||
await pred._predict_single(999, backtest=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
assert call_args.get("backtest") is True, "backtest=True 应传递给 build_context"
|
||||
finally:
|
||||
pred.build_context = orig_build
|
||||
# 重点: build_context 被调用时传入 backtest=True 和正确的 cutoff
|
||||
assert call_args.get("backtest") is True, "backtest=True 应传递给 build_context"
|
||||
|
||||
|
||||
class TestBacktestXgNotVisible:
|
||||
@@ -175,8 +176,8 @@ class TestBacktestXgNotVisible:
|
||||
@pytest.mark.asyncio
|
||||
async def test_stats_slice_respects_cutoff_for_xg_availability(self):
|
||||
"""available_at > cutoff 的 xG 数据不应被切片使用。"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import MagicMock
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
cutoff = datetime(2026, 1, 13, 20, 0, tzinfo=timezone.utc) # match_date - 2天
|
||||
match_dt = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
|
||||
@@ -206,7 +207,6 @@ class TestBacktestXgNotVisible:
|
||||
header = _make_header(match_dt)
|
||||
|
||||
import src.llm.context_builder as cb
|
||||
orig_get_form = cb._get_form
|
||||
|
||||
async def mock_get_form(db, team_id, before, *, limit=10):
|
||||
# before=cutoff(1月13日),比赛在1月15日,满足 before 条件
|
||||
@@ -214,14 +214,10 @@ class TestBacktestXgNotVisible:
|
||||
return [hist_match]
|
||||
return []
|
||||
|
||||
cb._get_form = mock_get_form
|
||||
|
||||
try:
|
||||
with patch.object(cb, "_get_form", mock_get_form):
|
||||
result = await cb.stats_slice(header, limit=10, before=cutoff)
|
||||
text = str(result)
|
||||
# xG 在 cutoff 之后才 available,不应出现在切片
|
||||
assert "2.50" not in text, f"xG 2.50 不应在切片中(available_at > cutoff):\n{text}"
|
||||
# 但无比分时仍应显示进球数据
|
||||
assert "无比分数据" in text or "场均进球" in text, f"无比分时仍应显示基本数据:\n{text}"
|
||||
finally:
|
||||
cb._get_form = orig_get_form
|
||||
|
||||
Reference in New Issue
Block a user