Files
PlantAI-rag-new/sdk/app/test_history.py
T
jerryandClaude Fable 5 8c103ea382 feat(capabilities): S1+S2 机制地基+多轮记忆(能力框架/热加载/MCP Provider/history 线程化)
- capabilities/{manifests,registry,providers,install_manager}.py + tool_runner.py:
  三层机制(注册层/调用层/安装管理层),四 Provider,热加载差分刷新
- configs/capabilities.yaml: 唯一注册入口(明文热加载面)
- api.py: QARequest+history/capabilities, /capabilities/install loopback 路由
- llm_client.py: complete() tools 透传 + complete_raw_messages() 工具调用循环支持
- pipeline.py/answer_composer.py: history 线程化(只进 Dialogue Context,检索零回归)
- sdk: services/history.py(3轮/16k预算) + routers/qa.py session_id 剥离注入
- compile_qa.sh: rsync 允许子目录 + capabilities.yaml 入 install 清单
- tooling/cap/: fake_mcp_server.py + cap_selfcheck.py 机制自检
- requirements.lock: mcp 2.1.1 + 依赖 12 包

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 13:00:35 +08:00

106 lines
3.0 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.
"""test_history.py —— 多轮记忆组装单测(S2,SDK 层)。
覆盖:空 messages→[] / N 轮截断 / 预算截断保留最近一轮 user / streaming 排除 /
查询异常→空 history。
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.services.history import assemble_history # noqa: E402
class FakeRow(dict):
"""aiosqlite.Row 的最小替身(支持键访问)。"""
def __getattr__(self, key):
return self[key]
class FakeDB:
def __init__(self, rows=None, error=None):
self.rows = rows or []
self.error = error
async def fetchall(self, sql, params):
if self.error:
raise self.error
# 模拟 SQL 的 status != 'streaming' 过滤(与真实查询一致)
return [r for r in self.rows if r.get("status") != "streaming"]
def row(role, text, status="done"):
return FakeRow(role=role, text=text, status=status)
@pytest.mark.asyncio
async def test_empty_messages():
db = FakeDB([])
assert await assemble_history(db, "s1") == []
@pytest.mark.asyncio
async def test_db_error_returns_empty():
db = FakeDB(error=RuntimeError("boom"))
assert await assemble_history(db, "s1") == []
@pytest.mark.asyncio
async def test_single_round():
db = FakeDB([row("user", "水稻SPL是什么?"), row("assistant", "答案是……")])
history = await assemble_history(db, "s1", rounds=3)
assert [m["role"] for m in history] == ["user", "assistant"]
@pytest.mark.asyncio
async def test_rounds_truncation():
rows = []
for i in range(5):
rows.append(row("user", f"问题{i}"))
rows.append(row("assistant", f"答案{i}"))
db = FakeDB(rows)
history = await assemble_history(db, "s1", rounds=3)
# 3 轮 = 3 user + 3 assistant(最近 3 轮)
assert [m["content"] for m in history if m["role"] == "user"] == ["问题2", "问题3", "问题4"]
@pytest.mark.asyncio
async def test_budget_truncation_keeps_last_user():
rows = [
row("user", "Q1"),
row("assistant", "A" * 20000), # 超大答案被截断
row("user", "Q2"),
]
db = FakeDB(rows)
history = await assemble_history(db, "s1", rounds=3, max_answer_chars=12000)
# 最近一轮 user(Q2)必须保留
assert history[-1]["role"] == "user"
assert history[-1]["content"] == "Q2"
@pytest.mark.asyncio
async def test_streaming_excluded():
rows = [
row("user", "Q1"),
row("assistant", "流式中…", status="streaming"),
row("user", "Q2"),
row("assistant", "完成答案"),
]
db = FakeDB(rows)
history = await assemble_history(db, "s1", rounds=3)
contents = [m["content"] for m in history]
assert "流式中…" not in contents
assert "完成答案" in contents
@pytest.mark.asyncio
async def test_empty_session_id():
db = FakeDB([])
assert await assemble_history(db, "") == []
assert await assemble_history(db, None) == []