Files
kplam 51dc8dc4f6 Initial commit: QMDSearch 分层信息检索服务
- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
2026-07-29 21:24:40 +08:00

135 lines
4.7 KiB
Python

"""数据模型与 taxonomy 加载的单元测试"""
import json
import pytest
from pydantic import ValidationError
from app.models.document import ChunkModel
from app.models.knowledge import (
UNCATEGORIZED,
CategoryResult,
TaxonomyCategory,
load_taxonomy,
)
from app.models.search import SearchHit, SearchRequest, SearchResponse
class TestLoadTaxonomy:
"""taxonomy 加载与校验"""
def test_default_taxonomy(self):
"""空路径时使用内置默认类目集"""
taxonomy = load_taxonomy("")
assert len(taxonomy) >= 6
names = [c.name for c in taxonomy]
assert UNCATEGORIZED in names
assert len(names) == len(set(names))
assert all(isinstance(c, TaxonomyCategory) for c in taxonomy)
def test_load_from_json_file(self, tmp_path):
"""从 JSON 文件加载自定义类目集"""
data = [
{"name": "技术", "description": "技术类文档"},
{"name": UNCATEGORIZED, "description": "未分类"},
]
file = tmp_path / "taxonomy.json"
file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
taxonomy = load_taxonomy(str(file))
assert [c.name for c in taxonomy] == ["技术", UNCATEGORIZED]
assert taxonomy[0].description == "技术类文档"
def test_append_uncategorized_when_missing(self, tmp_path):
"""JSON 文件缺少 uncategorized 时自动追加"""
data = [{"name": "技术"}, {"name": "产品"}]
file = tmp_path / "taxonomy.json"
file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
taxonomy = load_taxonomy(str(file))
names = [c.name for c in taxonomy]
assert names == ["技术", "产品", UNCATEGORIZED]
def test_duplicate_name_raises(self, tmp_path):
"""类目 name 重复时报错"""
data = [{"name": "技术"}, {"name": "技术"}]
file = tmp_path / "taxonomy.json"
file.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
with pytest.raises(ValueError, match="重复"):
load_taxonomy(str(file))
class TestCategoryResult:
"""CategoryResult 字段校验"""
def test_defaults(self):
result = CategoryResult(main_category="技术文档", confidence=0.9)
assert result.tags == []
assert result.confidence == 0.9
def test_confidence_out_of_range(self):
"""confidence 越界(>1 或 <0)时校验失败"""
with pytest.raises(ValidationError):
CategoryResult(main_category="技术文档", confidence=1.5)
with pytest.raises(ValidationError):
CategoryResult(main_category="技术文档", confidence=-0.1)
def test_confidence_boundary_values(self):
"""confidence 边界值 0 和 1 合法"""
assert CategoryResult(main_category="a", confidence=0).confidence == 0
assert CategoryResult(main_category="a", confidence=1).confidence == 1
class TestSearchModels:
"""检索模型字段校验"""
def test_search_hit(self):
hit = SearchHit(text="内容", doc_id="doc-1", score=0.85)
assert hit.title == ""
assert hit.section_path == ""
assert hit.doc_summary == ""
def test_search_hit_full_fields(self):
hit = SearchHit(
text="内容",
doc_id="doc-1",
title="标题",
section_path="第一章/第一节",
score=0.85,
doc_summary="一句话总结",
)
assert hit.section_path == "第一章/第一节"
def test_search_hit_missing_required(self):
"""缺少必填字段时校验失败"""
with pytest.raises(ValidationError):
SearchHit(text="内容", doc_id="doc-1") # type: ignore[call-arg]
def test_search_request_default_top_k(self):
req = SearchRequest(query="如何报销")
assert req.top_k is None
def test_search_response_defaults(self):
resp = SearchResponse(query="q", hits=[])
assert resp.routed_categories == []
assert resp.fallback is False
class TestChunkModel:
"""ChunkModel 字段校验"""
def test_defaults(self):
chunk = ChunkModel(doc_id="doc-1", chunk_index=0, text="第一段")
assert chunk.section_path == ""
def test_full_fields(self):
chunk = ChunkModel(doc_id="doc-1", chunk_index=2, text="第三段", section_path="第二章")
assert chunk.chunk_index == 2
assert chunk.section_path == "第二章"
def test_missing_required(self):
"""缺少必填字段时校验失败"""
with pytest.raises(ValidationError):
ChunkModel(doc_id="doc-1", chunk_index=0) # type: ignore[call-arg]