"""Summarizer 标题树接入的单元测试""" from app.core.summarizer import Summarizer from app.models.document import SummaryLevel class FakeOllama: """记录调用次数的 OllamaClient 替身""" def __init__(self) -> None: self.prompts: list[str] = [] async def generate(self, prompt: str) -> str: self.prompts.append(prompt) return "LLM 生成结果" def _structured_text() -> str: """构造带标题结构且长度 ≥ 500 的文档(不触发 2.5 回退)""" paragraph = "这是章节正文内容,包含足够多的信息量。" * 10 return f"# 安装指南\n{paragraph}\n## 环境准备\n{paragraph}\n## 安装步骤\n{paragraph}" class TestStructuredDocument: """结构化文档:L2 直接使用标题树,不调 LLM""" async def test_l2_from_headings_skips_llm(self): ollama = FakeOllama() summarizer = Summarizer(ollama=ollama) # type: ignore[arg-type] summary = await summarizer.summarize(_structured_text(), title="安装文档") assert summary.level == SummaryLevel.L3 # L1 + L3 两次调用,跳过了 L2 的 LLM 调用 assert len(ollama.prompts) == 2 # 大纲由标题树渲染,包含标题文本与层级缩进 assert summary.l2_outline is not None assert "- 安装指南" in summary.l2_outline assert " - 环境准备" in summary.l2_outline assert " - 安装步骤" in summary.l2_outline assert summary.l2_outline != "LLM 生成结果" # L3 的 prompt 中注入了标题树大纲 assert "安装指南" in ollama.prompts[1] class TestPlainDocument: """无结构文档:L2 回退 LLM 生成""" async def test_l2_falls_back_to_llm(self): ollama = FakeOllama() summarizer = Summarizer(ollama=ollama) # type: ignore[arg-type] text = "这是一段没有标题结构的正文内容," * 40 # 640 字符,不触发 2.5 回退 summary = await summarizer.summarize(text, title="") assert summary.level == SummaryLevel.L3 # L1 + L2 + L3 三次调用 assert len(ollama.prompts) == 3 assert summary.l2_outline == "LLM 生成结果" class TestFallbackUnaffected: """2.5 级回退逻辑不受标题树改造影响""" async def test_short_structured_text_still_fallback(self): ollama = FakeOllama() summarizer = Summarizer(ollama=ollama) # type: ignore[arg-type] text = "# 标题一\n简短内容。\n# 标题二\n简短内容。" summary = await summarizer.summarize(text, title="") assert summary.level == SummaryLevel.L2_HALF assert summary.l2_outline is None # L1 + L2.5 两次调用 assert len(ollama.prompts) == 2