from __future__ import annotations import types as t import pytest from genesis.inference.engine import InferenceEngine from genesis.inference.token import approximate_token_count from genesis.qa.guardrails import ( DEFAULT_MAX_QA_ROUNDS, QALoopController, resolve_qa_model, ) from tests.inference_helpers import FakeLLMClient class QAModels: """模拟 config.InferenceModels:primary=deepseek / fallback=qwen。""" def __init__(self): self.primary = t.SimpleNamespace(name="deepseek-chat", provider="deepseek") self.fallback = t.SimpleNamespace(name="qwen-max", provider="qwen") class QAModelsNoFallback: """无 fallback 配置(覆盖 None 分支)。""" def __init__(self): self.primary = t.SimpleNamespace(name="deepseek-chat") self.fallback = None class QAModelsFallbackEmptyName: """fallback 存在但 name 为空字符串(覆盖 name 假值分支)。""" def __init__(self): self.primary = t.SimpleNamespace(name="deepseek-chat") self.fallback = t.SimpleNamespace(name="") # ---------- resolve_qa_model(OV6:强制 fallback,防自校验盲区) ---------- def test_resolve_qa_model_returns_fallback_name(): assert resolve_qa_model(QAModels()) == "qwen-max" def test_resolve_qa_model_never_returns_primary(): """关键护栏:QA 不得返回主模型名(否则 DeepSeek 校验 DeepSeek 盲区)。""" models = QAModels() assert resolve_qa_model(models) != getattr(models.primary, "name", None) def test_resolve_qa_model_no_fallback_returns_none(): """无 fallback 时返回 None,迫使调用方显式指定独立校验模型。""" assert resolve_qa_model(QAModelsNoFallback()) is None def test_resolve_qa_model_fallback_empty_name_returns_none(): """fallback 存在但 name 为空时返回 None(不静默回退到 primary)。""" assert resolve_qa_model(QAModelsFallbackEmptyName()) is None def test_resolve_qa_model_none_models_returns_none(): assert resolve_qa_model(None) is None # ---------- QALoopController(OV6:循环边界防无限) ---------- def test_loop_default_max_rounds(): ctrl = QALoopController() assert ctrl.max_rounds == DEFAULT_MAX_QA_ROUNDS def test_loop_stops_at_max_rounds(): ctrl = QALoopController(max_rounds=3) assert ctrl.can_continue() ctrl.advance() ctrl.advance() ctrl.advance() assert ctrl.round == 3 assert ctrl.is_exhausted() assert not ctrl.can_continue() def test_loop_exhausted_cannot_continue(): ctrl = QALoopController(max_rounds=1) ctrl.advance() assert ctrl.is_exhausted() assert not ctrl.can_continue() # ---------- 集成:QA 调用强制走 fallback 模型 ---------- @pytest.mark.anyio async def test_qa_call_uses_fallback_not_primary(): client = FakeLLMClient([("ok", "QA 通过")]) models = QAModels() eng = InferenceEngine( client=client, models=models, estimator=approximate_token_count, ) qa_model = resolve_qa_model(models) assert qa_model == "qwen-max" await eng.chat( session_id="qa1", prompt="请校验以下章节", variables={}, model=qa_model, ) called_models = [c["model"] for c in client.calls] # 实际 LLM 调用必须用 fallback,绝不可落到 primary assert called_models == ["qwen-max"] assert "deepseek-chat" not in called_models