Files
qihuaiflow/backend/python/tests/test_regression_r1r2r3.py
T

88 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Phase 1 回归用例 R1/R2/R3 — 固化三个反复出现的问题(适配 chat.py 拆分后)。
R1: 技能列表不得被硬性截断为 15 条(微信 Bot 曾只返回 15 个技能)。
R2: 显式选中技能/MCP 时,system prompt 必须注入「最高优先级覆盖规则」。
R3: 媒体生成(图像/视频)必须有超时保护,超时后回写用户可懂的提示而非永久挂起。
Phase 1 已将 chat.py 巨石拆分为 chat_{utils,media,agents,skill,mcp,context,knowledge,service}.py,
因此本文件采用「扫描整个 app/api 包」做源码契约守卫,并对行为级用例从正确的子模块导入。
"""
import asyncio
import os
import glob
from unittest.mock import MagicMock
import pytest
_HERE = os.path.dirname(os.path.abspath(__file__))
API_DIR = os.path.abspath(os.path.join(_HERE, "..", "app", "api"))
def _read_api_package():
parts = []
for p in sorted(glob.glob(os.path.join(API_DIR, "chat*.py"))):
with open(p, "r", encoding="utf-8") as f:
parts.append(f.read())
return "\n".join(parts)
# ---------- R1 ----------
def test_r1_skill_list_not_hard_capped_at_15():
"""微信 Bot 技能列表曾因 limit(15) 只返回 15 个,必须防止回归。"""
src = _read_api_package()
assert "limit(15)" not in src, "检测到 limit(15),技能列表可能被硬性截断"
assert "LIMIT 15" not in src, "检测到 LIMIT 15,技能列表可能被硬性截断"
# ---------- R2 ----------
def test_r2_explicit_selection_override_rule_present():
"""显式选中技能/MCP 时,system prompt 必须包含最高优先级覆盖规则。
注:_build_system_prompt 已随 Phase 1 拆分迁入 chat_context.py,故扫描整个 api 包。
"""
src = _read_api_package()
assert "最高优先级覆盖规则" in src, "显式选中工具的覆盖规则文本缺失"
assert "只基于上文提供的该工具具体资料作答" in src, "覆盖规则应要求只基于已选工具资料作答"
assert "平台知识库" in src and "不要引用" in src, "覆盖规则应明确禁止引用平台知识库通用描述"
# ---------- R3 ----------
@pytest.mark.asyncio
async def test_r3_media_generation_timeout_returns_user_message(monkeypatch):
"""媒体生成超时必须回写用户可懂的超时提示,而非让前端永远『生成中…』。
_generate_media_task 位于 chat_media(经 _send_message_core 的 asyncio.create_task 调用),
本身是 fire-and-forget、返回 None,超时保护体现在把提示回写到消息 content。
"""
from app.api import chat_media as media_mod
class FakeModel:
id = "m_test"
capabilities = ["image"] # 图像 -> 90s 超时
msg_mock = MagicMock() # 捕获被回写的消息对象
fake_session = MagicMock()
def _query_side_effect(model_cls):
q = MagicMock()
if model_cls is media_mod.AIModel:
q.filter.return_value.first.return_value = FakeModel()
elif model_cls is media_mod.Message:
q.filter.return_value.first.return_value = msg_mock
return q
fake_session.query.side_effect = _query_side_effect
monkeypatch.setattr(media_mod, "SessionLocal", lambda: fake_session)
# 内部生成调用直接抛出 TimeoutError,等价模拟「生成耗时超过超时阈值」
async def _hang(db, ai_model, prompt, user_id):
raise asyncio.TimeoutError()
monkeypatch.setattr(media_mod, "_generate_media", _hang)
await media_mod._generate_media_task("msg1", "m_test", "画一只猫", "u1")
# 副作用:消息 content 被回写为用户可懂的超时提示
assert msg_mock.content is not None, "超时后必须回写消息内容,不能让前端永远『生成中…』"
assert "超时" in str(msg_mock.content), "媒体生成超时应回写用户可懂的超时提示"