"""Regression R4: 防止能力路由误判回归。 R4.1: detector_media_intent 对纯文本输入返回 None(不误判为 image/video)。 R4.2: resolve_auto_model("text") 返回有 text 能力的模型。 R4.3: 普通多轮对话必须走 text 模型,不可走 image/video。 R4.4: 所有已注册能力可由 resolve_auto_model 解析到启用模型或 None。 R4.5: is_default 模型必须存在且启用。 """ import json import pytest import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from app.api.chat_media import detect_media_intent, detect_ambiguous_creative_intent from app.capability_registry import resolve_auto_model, get_capability_defaults, CAPABILITY_TYPES, TEXT_FAMILY from app.database import SessionLocal from app.models import AIModel @pytest.fixture(scope="module") def db(): """获得独立测试数据库会话""" db = SessionLocal() yield db db.close() # ─── R4.1: detect_media_intent regression suite ─── MEDIA_INTENT_CASES = [ # (input, expected_intent) ("你好", None), ("今天天气怎么样", None), ("帮我写一篇文章", None), ("平台有哪些技能能", None), # 历史 bug:曾被误判为 image ("新对话", None), # 新建对话入口 ("画一只小猫", "image"), ("生成一张海报", "image"), ("给我画个头像", "image"), ("生成一个视频介绍", "video"), ("做一段短片", "video"), ("来段视频", "video"), ("朗读这段文字", "audio"), ("语音合成一段播报", "audio"), ("你能生成视频吗?", None), # 疑问句不触发 ("怎么画一只猫?", None), ("如何生成图片呢?", None), ("画一只小猫,要可爱的", "image"), # 有明确指令 ("Generate a cat image", "image"), # 英文 ("help me create a video", "video"), ] class TestR4_1_MediaIntent: @pytest.mark.parametrize("text,expected", MEDIA_INTENT_CASES) def test_detect_media_intent(self, text, expected): result = detect_media_intent(text) assert result == expected, f"detect_media_intent({text!r}) = {result!r}, expected {expected!r}" # ─── R4.2: resolve_auto_model 回归 ─── class TestR4_2_AutoModel: def test_text_model_has_text_capability(self, db): """文本对话默认模型必须有 text 能力""" m = resolve_auto_model("text", db) assert m is not None, "resolve_auto_model('text') returned None" caps = [c.lower() for c in (m.capabilities or [])] assert "text" in caps, f"Text model {m.name} has no 'text' capability: {caps}" def test_text_model_not_image_video(self, db): """文本默认模型不应触发 image/video 误判""" m = resolve_auto_model("text", db) caps = [c.lower() for c in (m.capabilities or [])] if "image" in caps or "video" in caps: # If text model has image caps, detect_media_intent must catch it assert detect_media_intent("你好") is None, "Normal text detected as media despite model having image caps" assert detect_media_intent("新对话") is None def test_thinking_model_exists(self, db): m = resolve_auto_model("thinking", db) assert m is not None, "resolve_auto_model('thinking') returned None" def test_all_11_capabilities_resolvable(self, db): """所有 11 个能力至少有一个模型或 None""" for cap_def in CAPABILITY_TYPES: cap = cap_def["id"] m = resolve_auto_model(cap, db) is_text_family = cap in TEXT_FAMILY if is_text_family: assert m is not None, f"{cap} is TEXT_FAMILY but resolve_auto_model returned None" # media/embedding/rerank/asr/ocr can legitimately be None if no model # ─── R4.3: is_default model exists ─── class TestR4_3_DefaultModel: def test_default_model_exists(self, db): m = db.query(AIModel).filter(AIModel.is_default == True, AIModel.is_enabled == True).first() assert m is not None, "No is_default=true and enabled model" caps = [c.lower() for c in (m.capabilities or [])] assert "text" in caps, f"Default model {m.name} must have 'text' capability: {caps}" def test_default_model_resolvable(self, db): m = db.query(AIModel).filter(AIModel.is_default == True, AIModel.is_enabled == True).first() assert m.id == resolve_auto_model("text", db).id, "is_default model != resolve_auto_model('text')" # ─── R4.4: capability defaults consistency ─── class TestR4_4_CapabilityDefaults: def test_text_defaults_in_sync_with_auto(self, db): """site_settings text capability defaults must match resolve_auto_model""" defaults = get_capability_defaults(db) for cap in ("text", "thinking"): mid = defaults.get(cap) if mid: m = resolve_auto_model(cap, db) assert m is not None, f"SiteSetting {cap}->{mid} not resolvable" assert m.id == mid, f"Mismatch: site_setting {cap}={mid} but resolve_auto_model returns {m.id}" def test_no_disabled_models_in_defaults(self, db): defaults = get_capability_defaults(db) for cap, mid in defaults.items(): if mid: m = db.query(AIModel).filter(AIModel.id == mid, AIModel.is_enabled == True).first() assert m is not None, f"Capability {cap} points to disabled/nonexistent model {mid}" # ─── R4.5: detect_ambiguous_creative_intent smoke ─── class TestR4_5_AmbiguousIntent: def test_ambiguous_detected(self): r = detect_ambiguous_creative_intent("帮我做一个东西") assert r is not None, "Ambiguous creative intent not detected" def test_explicit_not_ambiguous(self): r = detect_ambiguous_creative_intent("帮我写一篇文章") assert r is None, "Article request should not be ambiguous" def test_media_not_ambiguous(self): r = detect_ambiguous_creative_intent("画一只小猫") assert r is None, "Image intent should bypass ambiguous detection"