"""P1-C 回归测试: 公开预测仅 live+success,且不含 reasoning/agent_outputs。 运行: pytest tests/test_p1_c_public_predictions.py -v (使用 fake DB,无需真实 PG。) """ from __future__ import annotations from datetime import datetime, timezone from unittest.mock import MagicMock import pytest from fastapi.testclient import TestClient from src.api.app import app from src.api.deps import require_admin from src.db.models import League, Match, MatchStats, Prediction, Team class _FakeResult: def __init__(self, items): self._items = list(items) def scalars(self): class _S: def __init__(self, items): self._items = items def all(self): return list(self._items) return _S(self._items) def scalar_one_or_none(self): return self._items[0] if self._items else None def scalar(self): return self._items[0] if self._items else None class _FakeDB: """假 DB:捕获发往 Prediction 的查询语句,供测试断言 SQL 过滤条件。""" captured_pred_stmts: list = [] def __init__(self, match=None, predictions=()): self._match = match self._predictions = list(predictions) async def execute(self, stmt): # 根据 column_descriptions 判断查询实体 try: entity = stmt.column_descriptions[0]["entity"] except (IndexError, KeyError): entity = None if entity is Prediction: _FakeDB.captured_pred_stmts.append(stmt) return _FakeResult(self._predictions) return _FakeResult([self._match] if self._match else []) async def get(self, cls, mid): return self._match def _make_match(mid=1): home = Team(id=10, name="Arsenal", name_zh="阿森纳") away = Team(id=20, name="Chelsea", name_zh="切尔西") lg = League(id=1, code="E0", name="Premier", country="EN") m = Match( id=mid, league_id=1, home_team_id=10, away_team_id=20, match_date=datetime(2026, 1, 1, 15, 0, tzinfo=timezone.utc), match_status="finished", ) m.league = lg m.home_team = home m.away_team = away m.stats = MatchStats(match_id=mid, home_xg=1.5, away_xg=1.0) return m def _make_pred(pid, match_id, run_type="live", status="success", **overrides): p = Prediction( id=pid, match_id=match_id, provider="openai", model="gpt-4o", prompt_version="v1", mode=run_type, run_type=run_type, status=status, pred_home_goals=2.0, pred_away_goals=1.0, pred_1x2="1", reasoning="内部推理细节", agent_outputs=[{"agent": "form"}], subjective_confidence=0.7, created_at=datetime(2026, 1, 2, 12, 0, tzinfo=timezone.utc), ) for k, v in overrides.items(): setattr(p, k, v) return p from src.db.base import get_db_read @pytest.fixture def client(): app.dependency_overrides[require_admin] = lambda: None return TestClient(app) class TestPublicPredictionsFilter: """P1-C: GET /matches/{id} 公开预测仅 run_type=live 且 status=success。""" def test_query_filters_by_run_type_and_status(self, client): """P1-C: 查询必须包含 run_type='live' AND status='success' 过滤。""" _FakeDB.captured_pred_stmts = [] m = _make_match(1) fake = _FakeDB(match=m, predictions=[_make_pred(1, 1)]) app.dependency_overrides[get_db_read] = lambda: fake try: resp = client.get("/api/v1/matches/1") assert resp.status_code == 200, resp.text # 验证发往 Prediction 的 SQL 含 run_type 与 status 过滤 assert _FakeDB.captured_pred_stmts, "未发出 Prediction 查询" sql = str(_FakeDB.captured_pred_stmts[0]).lower() assert "run_type" in sql, f"SQL 缺少 run_type 过滤: {sql}" assert "status" in sql, f"SQL 缺少 status 过滤: {sql}" finally: app.dependency_overrides.pop(get_db_read, None) def test_no_reasoning_or_agent_outputs(self, client): """P1-C: 公开预测不得含 reasoning/agent_outputs。""" m = _make_match(1) preds = [_make_pred(1, 1, run_type="live", status="success")] fake = _FakeDB(match=m, predictions=preds) app.dependency_overrides[get_db_read] = lambda: fake try: resp = client.get("/api/v1/matches/1") assert resp.status_code == 200, resp.text body = resp.json() assert len(body["recent_predictions"]) == 1 p = body["recent_predictions"][0] assert "reasoning" not in p, "公开预测不得含 reasoning" assert "agent_outputs" not in p, "公开预测不得含 agent_outputs" # 但核心字段保留 assert p["pred_home_goals"] == 2.0 assert p["pred_1x2"] == "1" finally: app.dependency_overrides.pop(get_db_read, None)