155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
from __future__ import annotations
|
||
|
||
from types import SimpleNamespace
|
||
|
||
import pytest
|
||
from openpyxl import load_workbook
|
||
|
||
from app.gateway.routers.llmwiki import _build_wiki_excel
|
||
from deerflow.integrations.weknora.local_index.retrieval import WikiRetrievalService, is_fully_vectorized
|
||
|
||
|
||
class _Store:
|
||
def __init__(self, states):
|
||
self.states = states
|
||
|
||
async def list_index_status(self, mapping_ids=None):
|
||
wanted = set(mapping_ids or [])
|
||
return [state for state in self.states if not wanted or state["knowledge_base_mapping_id"] in wanted]
|
||
|
||
|
||
class _WikiClient:
|
||
def __init__(self):
|
||
self.list_calls = []
|
||
self.raw_search_called = False
|
||
|
||
async def list_wiki_pages(self, knowledge_base_id, *, page=1, page_size=50, query=""):
|
||
self.list_calls.append((knowledge_base_id, query))
|
||
return {"pages": [{"slug": "guides/install", "title": "安装"}], "total": 1, "total_pages": 1}
|
||
|
||
async def get_wiki_page(self, knowledge_base_id, slug):
|
||
return {
|
||
"id": f"{knowledge_base_id}:{slug}",
|
||
"slug": slug,
|
||
"title": "安装",
|
||
"summary": "Wiki 摘要",
|
||
"content": "# 安装\n这是处理后的 Wiki 正文。",
|
||
"status": "published",
|
||
"category_path": ["指南"],
|
||
}
|
||
|
||
async def search(self, query, knowledge_base_ids):
|
||
self.raw_search_called = True
|
||
raise AssertionError("raw knowledge search must never be called")
|
||
|
||
|
||
class _VectorSearch:
|
||
def __init__(self):
|
||
self.embedding = SimpleNamespace(fingerprint="fp-current")
|
||
self.calls = []
|
||
|
||
async def search(self, query, mappings, **kwargs):
|
||
self.calls.append([mapping["id"] for mapping in mappings])
|
||
mapping = mappings[0]
|
||
return {
|
||
"results": [
|
||
{
|
||
"kind": "wiki_page",
|
||
"id": "local-page",
|
||
"wiki_page_id": "local-page",
|
||
"wiki_slug": "local/page",
|
||
"knowledge_base_id": mapping["id"],
|
||
"title": "本地页面",
|
||
"content": "本地 Wiki",
|
||
"matched_sections": [],
|
||
"score": 0.95,
|
||
}
|
||
],
|
||
"warnings": [],
|
||
}
|
||
|
||
|
||
def _ready_state(mapping_id: str):
|
||
return {
|
||
"knowledge_base_mapping_id": mapping_id,
|
||
"state": "idle",
|
||
"last_success_at": "2026-08-24T00:00:00Z",
|
||
"embedding_fingerprint": "fp-current",
|
||
"local_page_count": 1,
|
||
"ready_page_count": 1,
|
||
"failed_page_count": 0,
|
||
"vector_count": 3,
|
||
}
|
||
|
||
|
||
def test_fully_vectorized_requires_completed_full_consistent_scan() -> None:
|
||
state = _ready_state("kb")
|
||
assert is_fully_vectorized(state, "fp-current")
|
||
assert not is_fully_vectorized({**state, "state": "partial"}, "fp-current")
|
||
assert not is_fully_vectorized({**state, "ready_page_count": 0}, "fp-current")
|
||
assert not is_fully_vectorized(state, "different-fingerprint")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_unvectorized_library_uses_only_wiki_page_search_api() -> None:
|
||
client = _WikiClient()
|
||
service = WikiRetrievalService(_Store([]), vector_search=_VectorSearch(), client=client)
|
||
mapping = {"id": "local-kb", "weknora_id": "remote-kb", "name": "测试库"}
|
||
|
||
result = await service.search("安装", [mapping])
|
||
|
||
assert result["mode"] == "wiki_api"
|
||
assert result["fallback_knowledge_base_ids"] == ["local-kb"]
|
||
assert result["results"][0]["kind"] == "wiki_page"
|
||
assert result["results"][0]["content"] == "# 安装\n这是处理后的 Wiki 正文。"
|
||
assert "chunk_id" not in result["results"][0]
|
||
assert client.list_calls == [("remote-kb", "安装")]
|
||
assert not client.raw_search_called
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_mixed_search_vectors_complete_library_and_falls_back_incomplete_library() -> None:
|
||
client = _WikiClient()
|
||
vector = _VectorSearch()
|
||
service = WikiRetrievalService(_Store([_ready_state("ready")]), vector_search=vector, client=client)
|
||
mappings = [
|
||
{"id": "ready", "weknora_id": "remote-ready", "name": "已完成"},
|
||
{"id": "pending", "weknora_id": "remote-pending", "name": "未完成"},
|
||
]
|
||
|
||
result = await service.search("安装", mappings, top_k_pages=8)
|
||
|
||
assert result["mode"] == "mixed"
|
||
assert vector.calls == [["ready"]]
|
||
assert client.list_calls == [("remote-pending", "安装")]
|
||
assert {item["knowledge_base_id"] for item in result["results"]} == {"ready", "pending"}
|
||
|
||
|
||
def test_excel_export_contains_current_directory_and_every_parent_directory() -> None:
|
||
long_content = "正文" * 20_000
|
||
pages = [
|
||
{"id": "1", "slug": "root", "title": "根专题", "content": "根", "status": "published"},
|
||
{"id": "2", "slug": "root/child", "title": "子专题", "parent_slug": "root", "content": "子", "status": "published"},
|
||
{
|
||
"id": "3",
|
||
"slug": "root/child/article",
|
||
"title": "文章",
|
||
"parent_slug": "root/child",
|
||
"content": long_content,
|
||
"status": "published",
|
||
},
|
||
]
|
||
|
||
workbook = load_workbook(_build_wiki_excel("知识库", pages), read_only=True)
|
||
sheet = workbook["Wiki 全量导出"]
|
||
rows = list(sheet.iter_rows(values_only=True))
|
||
headings = list(rows[0])
|
||
article = dict(zip(headings, rows[3], strict=True))
|
||
|
||
assert article["目录完整路径"] == "根专题 / 子专题"
|
||
assert article["所在目录"] == "子专题"
|
||
assert article["父级目录完整路径"] == "根专题"
|
||
assert article["第1级目录"] == "根专题"
|
||
assert article["第2级目录"] == "子专题"
|
||
assert article["正文"] + article["正文(续1)"] == long_content
|