fix(P0-03): Prediction 幂等指纹——只追加,不覆盖
_upsert_prediction 改为 _insert_or_find_by_fingerprint: - 同 input_hash → 返回已有行(绝不 UPDATE pred_/reasoning/agent_outputs) - 不同 input_hash → INSERT 新行 input_hash 升级为规范 JSON SHA-256,捕获:match_id, cutoff, prompt_version, prompt_hash, system_prompt_hash, provider, model, mode, run_type, temperature, context_hash, agent_ids。移除旧 (match, provider, model, mode, run_type) 唯一约束, 改为 partial unique index(WHERE input_hash IS NOT NULL,兼容旧 NULL 数据)。 三条路径(single/multi/baseline)统一传足指纹字段。 迁移 0024 + 测试 test_p0_prediction_fingerprint(10/10);全量 295 通过。
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
验证:
|
||||
1. 唯一约束包含 mode + run_type
|
||||
2. 同一场比赛 live 与 backtest 预测可共存,互不覆盖
|
||||
3. _upsert_prediction 正确区分 run_type
|
||||
3. _insert_or_find_by_fingerprint 正确区分 run_type
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel
|
||||
import pytest
|
||||
from sqlalchemy import Index
|
||||
|
||||
from src.db.models import Prediction, UniqueConstraint, CheckConstraint
|
||||
|
||||
@@ -22,20 +23,32 @@ MIGRATION_PATH = REPO_ROOT / "alembic" / "versions" / "0013_predictions_unique_c
|
||||
|
||||
|
||||
class TestUniqueConstraint:
|
||||
"""验证唯一约束包含 mode + run_type。"""
|
||||
"""P0-03: 验证幂等指纹唯一索引(替代旧 (match, provider, model, 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
|
||||
def test_input_hash_partial_unique_index(self):
|
||||
"""P0-03: input_hash 非空时必须唯一(同指纹 → 返回已有行,不 UPDATE/INSERT)。"""
|
||||
idx = [
|
||||
i for i in Prediction.__table__.indexes
|
||||
if i.unique and "input_hash" in i.name
|
||||
]
|
||||
assert len(uc) == 1
|
||||
cols = [c.name for c in uc[0].columns]
|
||||
assert cols == ["match_id", "provider", "model", "mode", "run_type"]
|
||||
assert len(idx) == 1, f"缺少 input_hash partial unique 索引,现有 indexes: {[i.name for i in Prediction.__table__.indexes]}"
|
||||
# partial unique: postgresql_where 必须限制 input_hash IS NOT NULL
|
||||
assert idx[0].dialect_kwargs.get("postgresql_where") is not None
|
||||
|
||||
def test_old_unique_constraint_removed(self):
|
||||
"""P0-03: 旧 (match, provider, model, mode, run_type) 唯一约束必须已移除。"""
|
||||
from sqlalchemy import UniqueConstraint
|
||||
|
||||
old = [
|
||||
c for c in Prediction.__table__.constraints
|
||||
if isinstance(c, UniqueConstraint) and c.name == "uq_predictions_match_provider_model_mode_run_type"
|
||||
]
|
||||
assert len(old) == 0, f"旧约束必须已移除,但仍存在: {[c.name for c in old]}"
|
||||
|
||||
def test_run_type_check_constraint(self):
|
||||
"""应有 run_type 的 check constraint。"""
|
||||
from sqlalchemy import CheckConstraint
|
||||
|
||||
cc = [
|
||||
c for c in Prediction.__table__.constraints
|
||||
if isinstance(c, CheckConstraint) and "run_type" in c.name
|
||||
@@ -52,13 +65,16 @@ class TestUniqueConstraint:
|
||||
|
||||
|
||||
class TestUpsertPredictionSignature:
|
||||
"""验证 _upsert_prediction 函数签名包含 run_type。"""
|
||||
"""验证 _insert_or_find_by_fingerprint 签名(P0-03 指纹模式)。"""
|
||||
|
||||
def test_signature_has_run_type(self):
|
||||
from src.llm.predict import _upsert_prediction
|
||||
def test_signature_uses_values_dict(self):
|
||||
"""P0-03: 新接口通过 values dict 接收全部字段(含 run_type/match_id/...)。"""
|
||||
from src.llm.predict import _insert_or_find_by_fingerprint
|
||||
|
||||
sig = inspect.signature(_upsert_prediction)
|
||||
assert "run_type" in sig.parameters
|
||||
sig = inspect.signature(_insert_or_find_by_fingerprint)
|
||||
params = sig.parameters
|
||||
assert "session" in params
|
||||
assert "values" in params # 所有业务字段走 values dict
|
||||
|
||||
def test_signature_has_backtest_in_predict_match(self):
|
||||
from src.llm.predict import predict_match
|
||||
|
||||
Reference in New Issue
Block a user