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:
@@ -62,7 +62,7 @@ async def test_predict_baseline_returns_predict_result():
|
||||
async def __aexit__(self, *a):
|
||||
return None
|
||||
|
||||
# P3-2:baseline 在服务层落库(get_uow + _upsert_prediction),需 mock 掉。
|
||||
# P3-2:baseline 在服务层落库(get_uow + _insert_or_find_by_fingerprint),需 mock 掉。
|
||||
class FakeUoW:
|
||||
async def __aenter__(self):
|
||||
return _make_session()
|
||||
@@ -72,24 +72,24 @@ async def test_predict_baseline_returns_predict_result():
|
||||
|
||||
captured = {}
|
||||
|
||||
async def fake_upsert(session, **kw):
|
||||
captured.update(kw)
|
||||
async def fake_upsert(session, *, values):
|
||||
captured.update(values)
|
||||
return SimpleNamespace(id=77)
|
||||
|
||||
# baseline.py 内部 from-import get_uow / _upsert_prediction,需 patch 真实来源模块。
|
||||
# baseline.py 内部 from-import get_uow / _insert_or_find_by_fingerprint,需 patch 真实来源模块。
|
||||
with patch("src.llm.baseline._avg_goals", fake_avg), \
|
||||
patch("src.llm.baseline.AsyncSessionLocal") as SLC, \
|
||||
patch("src.db.unit_of_work.get_uow", FakeUoW), \
|
||||
patch("src.llm.baseline._upsert_prediction", fake_upsert):
|
||||
patch("src.llm.baseline._insert_or_find_by_fingerprint", fake_upsert):
|
||||
SLC.return_value = FakeCM()
|
||||
|
||||
result = await predict_baseline(1)
|
||||
|
||||
# P3-2:验证服务层落库被调用且属性映射正确
|
||||
assert captured["match_id"] == 1
|
||||
assert captured["provider_name"] == "baseline"
|
||||
assert captured["provider"] == "baseline"
|
||||
assert captured["run_type"] == "live"
|
||||
assert captured["values"]["pred_home_goals"] == 2.0
|
||||
assert captured["pred_home_goals"] == 2.0
|
||||
|
||||
assert isinstance(result, PredictResult)
|
||||
assert result.mode == "baseline"
|
||||
@@ -225,8 +225,8 @@ async def test_baseline_service_persists_with_correct_attributes(monkeypatch):
|
||||
"""P3-2:baseline 在服务层(predict_baseline)落库,属性映射与路由旧版一致。"""
|
||||
captured = {}
|
||||
|
||||
async def fake_upsert(session, **kwargs):
|
||||
captured.update(kwargs)
|
||||
async def fake_upsert(session, *, values):
|
||||
captured.update(values)
|
||||
return SimpleNamespace(id=77)
|
||||
|
||||
class FakeMatch:
|
||||
@@ -253,49 +253,47 @@ async def test_baseline_service_persists_with_correct_attributes(monkeypatch):
|
||||
monkeypatch.setattr("src.llm.baseline._avg_goals", fake_avg)
|
||||
monkeypatch.setattr("src.llm.baseline.AsyncSessionLocal", FakeSLC)
|
||||
monkeypatch.setattr("src.db.unit_of_work.get_uow", lambda: _FakeUoW())
|
||||
# baseline.py 模块级 import _upsert_prediction(第 15 行),需 patch baseline 模块属性
|
||||
monkeypatch.setattr("src.llm.baseline._upsert_prediction", fake_upsert)
|
||||
# baseline.py 模块级 import _insert_or_find_by_fingerprint(第 15 行),需 patch baseline 模块属性
|
||||
monkeypatch.setattr("src.llm.baseline._insert_or_find_by_fingerprint", fake_upsert)
|
||||
|
||||
result = await predict_baseline(1)
|
||||
|
||||
# 落库被调用且属性映射正确
|
||||
assert captured, f"predict_baseline 应调用 _upsert_prediction 落库,但 captured 为空(result.prediction_id={result.prediction_id!r})"
|
||||
assert captured, f"predict_baseline 应调用 _insert_or_find_by_fingerprint 落库,但 captured 为空(result.prediction_id={result.prediction_id!r})"
|
||||
assert captured["match_id"] == 1
|
||||
assert captured["provider_name"] == "baseline"
|
||||
assert captured["provider"] == "baseline"
|
||||
assert captured["model"] == "baseline"
|
||||
assert captured["mode"] == "baseline"
|
||||
assert captured["run_type"] == "live"
|
||||
v = captured["values"]
|
||||
assert v["prompt_version"] == "baseline_v1"
|
||||
assert v["pred_home_goals"] == 2.0
|
||||
assert v["pred_away_goals"] == 1.0
|
||||
assert v["pred_1x2"] == "1"
|
||||
assert v["subjective_confidence"] == 0.5
|
||||
assert v["prompt_tokens"] == 0
|
||||
assert v["completion_tokens"] == 0
|
||||
assert v["latency_ms"] == 0
|
||||
assert v["raw_response"] == {"home_avg": 2.0, "away_avg": 1.0}
|
||||
assert v["status"] == "success"
|
||||
assert captured["prompt_version"] == "baseline_v1"
|
||||
assert captured["pred_home_goals"] == 2.0
|
||||
assert captured["pred_away_goals"] == 1.0
|
||||
assert captured["pred_1x2"] == "1"
|
||||
assert captured["subjective_confidence"] == 0.5
|
||||
assert captured["prompt_tokens"] == 0
|
||||
assert captured["completion_tokens"] == 0
|
||||
assert captured["latency_ms"] == 0
|
||||
assert captured["raw_response"] == {"home_avg": 2.0, "away_avg": 1.0}
|
||||
assert captured["status"] == "success"
|
||||
|
||||
# 回填真实 prediction_id(服务层落库后取得)
|
||||
assert result.prediction_id == 77
|
||||
assert result.pred_1x2 == "1"
|
||||
assert captured["match_id"] == 1
|
||||
assert captured["provider_name"] == "baseline"
|
||||
assert captured["provider"] == "baseline"
|
||||
assert captured["model"] == "baseline"
|
||||
assert captured["mode"] == "baseline"
|
||||
assert captured["run_type"] == "live"
|
||||
v = captured["values"]
|
||||
assert v["prompt_version"] == "baseline_v1"
|
||||
assert v["pred_home_goals"] == 2.0
|
||||
assert v["pred_away_goals"] == 1.0
|
||||
assert v["pred_1x2"] == "1"
|
||||
assert v["subjective_confidence"] == 0.5
|
||||
assert v["prompt_tokens"] == 0
|
||||
assert v["completion_tokens"] == 0
|
||||
assert v["latency_ms"] == 0
|
||||
assert v["raw_response"] == {"home_avg": 2.0, "away_avg": 1.0}
|
||||
assert v["status"] == "success"
|
||||
assert captured["prompt_version"] == "baseline_v1"
|
||||
assert captured["pred_home_goals"] == 2.0
|
||||
assert captured["pred_away_goals"] == 1.0
|
||||
assert captured["pred_1x2"] == "1"
|
||||
assert captured["subjective_confidence"] == 0.5
|
||||
assert captured["prompt_tokens"] == 0
|
||||
assert captured["completion_tokens"] == 0
|
||||
assert captured["latency_ms"] == 0
|
||||
assert captured["raw_response"] == {"home_avg": 2.0, "away_avg": 1.0}
|
||||
assert captured["status"] == "success"
|
||||
|
||||
# 回填真实 prediction_id(服务层落库后取得)
|
||||
assert result.prediction_id == 77
|
||||
|
||||
Reference in New Issue
Block a user