P0 严重问题修复: - 修复 form_slice/stats_slice 主客身份反转(历史比赛视角错误) - 修复 understat.py httpx 未导入导致的 NameError - 修复 LLM 解析失败时静默产生假成功预测(0-0 平局+置信度0.5) 预测路径修复: - multi-agent 路径增加 backtest cutoff 透传,回测防泄漏生效 - H2H 切片汇总统计改为从当前主队视角计数 - 预测唯一约束增加 mode+run_type 维度,防止回测覆盖实盘预测 伤停管线修复: - IntegrityError 后不再整批回滚丢数据(改用逐条 flush) - return_date 正确解析并写入 - retrieved_at 比较统一用 date() 避免当天数据不可见 - 唯一索引改为 partial unique index(排除 NULL 重复) - HTTP 缓存 TTL 从 7 天改为 6 小时 安全与连接管理: - /api/v1/predict 增加内存滑动窗口限流(10次/分钟/IP) - 预测路由改用短 session 模式,LLM 调用期间不持有 DB 连接 Docker 部署修复: - 修复 .dockerignore 排除 *.md 导致 COPY README.md 失败 - 容器内 DATABASE_URL 使用 postgres 服务名(非 localhost) - 启动时自动执行 alembic upgrade head - 前端改用多阶段构建(Dockerfile.frontend) 新增测试(5个文件,24+用例): - test_p0_home_away.py: 主客身份反转回归测试 - test_p0_parse_failure.py: LLM 解析失败回归测试 - test_multi_agent_cutoff.py: multi-agent cutoff 透传测试 - test_h2h_perspective.py: H2H 视角测试 - test_injuries_pipeline.py: 伤停管线 5 项修复测试 - test_predict_protection.py: 限流+短 session 测试 - test_prediction_unique_constraint.py: 唯一约束测试 迁移: - 0012_injuries_partial_unique_and_return_date.py - 0013_predictions_unique_constraint_mode_run_type.py
122 lines
4.6 KiB
Python
122 lines
4.6 KiB
Python
"""回归测试: 预测唯一约束修复 —— live 与 backtest 可共存。
|
|
|
|
验证:
|
|
1. 唯一约束包含 mode + run_type
|
|
2. 同一场比赛 live 与 backtest 预测可共存,互不覆盖
|
|
3. _upsert_prediction 正确区分 run_type
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
|
|
from pydantic import BaseModel
|
|
import pytest
|
|
|
|
from src.db.models import Prediction, UniqueConstraint, CheckConstraint
|
|
|
|
|
|
class TestUniqueConstraint:
|
|
"""验证唯一约束包含 mode + run_type。"""
|
|
|
|
def test_constraint_columns(self):
|
|
"""唯一约束应包含 match_id, provider, model, mode, run_type。"""
|
|
uc = [
|
|
c for c in Prediction.__table__.constraints
|
|
if isinstance(c, UniqueConstraint) and "match" in c.name
|
|
]
|
|
assert len(uc) == 1
|
|
cols = [c.name for c in uc[0].columns]
|
|
assert cols == ["match_id", "provider", "model", "mode", "run_type"]
|
|
|
|
def test_run_type_check_constraint(self):
|
|
"""应有 run_type 的 check constraint。"""
|
|
cc = [
|
|
c for c in Prediction.__table__.constraints
|
|
if isinstance(c, CheckConstraint) and "run_type" in c.name
|
|
]
|
|
assert len(cc) == 1
|
|
|
|
def test_run_type_column_exists(self):
|
|
"""run_type 列应存在且 NOT NULL,默认 'live'。"""
|
|
cols = {c.name: c for c in Prediction.__table__.columns}
|
|
assert "run_type" in cols
|
|
assert cols["run_type"].nullable is False
|
|
# 默认值
|
|
assert cols["run_type"].default.arg == "live" if cols["run_type"].default else True
|
|
|
|
|
|
class TestUpsertPredictionSignature:
|
|
"""验证 _upsert_prediction 函数签名包含 run_type。"""
|
|
|
|
def test_signature_has_run_type(self):
|
|
from src.llm.predict import _upsert_prediction
|
|
|
|
sig = inspect.signature(_upsert_prediction)
|
|
assert "run_type" in sig.parameters
|
|
|
|
def test_signature_has_backtest_in_predict_match(self):
|
|
from src.llm.predict import predict_match
|
|
|
|
sig = inspect.signature(predict_match)
|
|
assert "backtest" in sig.parameters
|
|
|
|
def test_signature_has_backtest_in_predict_multi(self):
|
|
from src.llm.agents.orchestrator import predict_match_multi
|
|
|
|
sig = inspect.signature(predict_match_multi)
|
|
assert "backtest" in sig.parameters
|
|
|
|
|
|
class TestMigration:
|
|
"""验证迁移文件存在且内容正确。"""
|
|
|
|
def test_migration_exists(self):
|
|
import os
|
|
|
|
path = "/.octop/workspaces/CA7PFH/Profeto/alembic/versions/0013_predictions_unique_constraint_mode_run_type.py"
|
|
assert os.path.exists(path)
|
|
|
|
def test_migration_adds_column_and_constraint(self):
|
|
path = "/.octop/workspaces/CA7PFH/Profeto/alembic/versions/0013_predictions_unique_constraint_mode_run_type.py"
|
|
content = open(path).read()
|
|
|
|
assert 'run_type' in content
|
|
assert 'uq_predictions_match_provider_model_mode_run_type' in content
|
|
assert 'backtest' in content
|
|
assert 'live' in content
|
|
# 验证数据回填逻辑
|
|
assert "UPDATE predictions SET run_type = 'live'" in content
|
|
|
|
|
|
class TestLiveBacktestCoexist:
|
|
"""验证 live 与 backtest 可共存(逻辑验证,无需数据库)。"""
|
|
|
|
def test_different_run_type_allow_coexistence(self):
|
|
"""同一 match_id + provider + model + mode,不同 run_type 应可共存。
|
|
|
|
这是核心修复:之前唯一约束只有 (match_id, provider, model),
|
|
backtest 会覆盖 live 预测。
|
|
"""
|
|
# 模拟两行数据
|
|
class FakeRow:
|
|
def __init__(self, **kw):
|
|
for k, v in kw.items():
|
|
setattr(self, k, v)
|
|
|
|
live = FakeRow(match_id=1, provider="openai", model="gpt-4o", mode="single", run_type="live")
|
|
backtest = FakeRow(match_id=1, provider="openai", model="gpt-4o", mode="single", run_type="backtest")
|
|
|
|
# 两者唯一键不同(因为 run_type 不同)
|
|
live_key = (live.match_id, live.provider, live.model, live.mode, live.run_type)
|
|
backtest_key = (backtest.match_id, backtest.provider, backtest.model, backtest.mode, backtest.run_type)
|
|
|
|
assert live_key != backtest_key, "live 与 backtest 应有不同的唯一键"
|
|
assert live_key == (1, "openai", "gpt-4o", "single", "live")
|
|
assert backtest_key == (1, "openai", "gpt-4o", "single", "backtest")
|
|
|
|
def test_same_run_type_prevents_duplicate(self):
|
|
"""相同 run_type 的重复预测仍应被约束阻止。"""
|
|
key1 = (1, "openai", "gpt-4o", "single", "live")
|
|
key2 = (1, "openai", "gpt-4o", "single", "live")
|
|
assert key1 == key2, "相同 run_type 应有相同唯一键,应被约束阻止"
|