51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
135 lines
4.7 KiB
Python
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]
|