"""回归测试: predictions 表 agent_weights 独立持久化。 验证: 1. ORM 模型有 agent_weights 列(JSONB, 可空) 2. 迁移文件存在且可逆 3. orchestrator 写入 agent_weights """ from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock, patch import pytest from src.db.models import Prediction # 仓库根目录下的 alembic 迁移目录 —— 相对本测试文件解析, # 避免硬编码某台机器/CI 上的绝对路径(见 tests/test_regressions.py 的 _read 约定)。 REPO_ROOT = Path(__file__).resolve().parent.parent MIGRATION_PATH = REPO_ROOT / "alembic" / "versions" / "0014_predictions_agent_weights.py" class TestAgentWeightsColumn: """验证 predictions 表有 agent_weights 列。""" def test_orm_has_agent_weights_column(self): """ORM 模型应包含 agent_weights 列。""" cols = {c.name: c for c in Prediction.__table__.columns} assert "agent_weights" in cols, "predictions 表应有 agent_weights 列" def test_agent_weights_is_jsonb(self): """agent_weights 应为 JSONB 类型。""" col = Prediction.__table__.columns["agent_weights"] # JSONB 类型检查 assert "JSON" in str(col.type).upper() or "JSONB" in str(col.type).upper() def test_agent_weights_nullable(self): """agent_weights 应可空(旧行保持 NULL)。""" col = Prediction.__table__.columns["agent_weights"] assert col.nullable is True, "agent_weights 应可空" class TestMigration: """验证迁移文件存在且内容正确。""" def test_migration_exists(self): assert MIGRATION_PATH.is_file(), f"迁移文件不存在: {MIGRATION_PATH}" def test_migration_content(self): content = MIGRATION_PATH.read_text(encoding="utf-8") assert "agent_weights" in content assert "upgrade" in content assert "downgrade" in content assert "downgrade" in content and "drop_column" in content assert "nullable=True" in content class TestOrchestratorWritesAgentWeights: """验证 orchestrator 写入 agent_weights。""" @pytest.mark.asyncio async def test_orchestrator_writes_agent_weights_to_upsert(self): """orchestrator 应将 agent_weights 传入 _insert_or_find_by_fingerprint。""" from src.llm.agents import orchestrator as orch_mod from src.llm.agents.base import AgentReport from src.llm.context_builder import MatchHeader header = MatchHeader( match_id=999, home_name="A", away_name="B", league_name="X", season=None, match_date="?", match_dt=None, stage=None, home_team_id=1, away_team_id=2, league_id=1, ) # 构造有效专家报告(至少 1 个 ok) reports = [ AgentReport(agent="form", status="ok", analysis="good"), AgentReport(agent="stats", status="error", analysis="failed"), AgentReport(agent="home_away", status="ok", analysis="good"), AgentReport(agent="standings", status="no_data", analysis="无数据"), AgentReport(agent="h2h", status="error", analysis="failed"), ] captured_values = {} async def mock_specialists(h, *, version, before, model_override=None): return reports async def mock_provider(aid, **kw): return MagicMock(model="test-model") async def mock_header(mid, db=None): return header async def mock_aggregator(header, reports, *, provider, version): return { "pred_home_goals": 2, "pred_away_goals": 1, "pred_1x2": "1", "subjective_confidence": 0.7, "reasoning": "test", "agent_weights": {"form": 0.3, "home_away": 0.5, "stats": 0.2}, }, 100, 50 async def mock_upsert(session, *, values): captured_values.update(values) p = MagicMock() p.id = 1 p.provider = "test" p.model = "test" p.prompt_version = "v1" p.pred_home_goals = 2 p.pred_away_goals = 1 p.pred_1x2 = "1" p.subjective_confidence = 0.7 p.reasoning = "test" p.agent_outputs = [] p.agent_weights = values.get("agent_weights") return p class FakeUow: async def __aenter__(self): return self async def __aexit__(self, *a): pass async def get(self, cls, id): return MagicMock() with patch.object(orch_mod, "run_specialists", mock_specialists), \ patch.object(orch_mod, "_agent_provider", mock_provider), \ patch.object(orch_mod, "load_match_header", mock_header), \ patch.object(orch_mod, "_insert_or_find_by_fingerprint", mock_upsert), \ patch.object(orch_mod, "run_aggregator", mock_aggregator), \ patch.object(orch_mod, "get_uow", FakeUow): result = await orch_mod.predict_match_multi(999) # 断言 agent_weights 被写入 assert "agent_weights" in captured_values, "agent_weights 应传入 _insert_or_find_by_fingerprint" assert captured_values["agent_weights"] is not None, "agent_weights 不应为 None" assert "form" in captured_values["agent_weights"], "agent_weights 应包含专家权重" print(f"PASS: agent_weights = {captured_values['agent_weights']}")