67157dff34
- 单元测试: Database 和 Service 层 - 集成测试: MCP 工具端到端测试 - 测试配置: pytest.ini 和覆盖率设置 - 测试文档: tests/README.md 测试覆盖: - 数据库 CRUD 操作 - FTS5 搜索和验证 - 缓存服务 (LRU + TTL) - 解析服务 (frontmatter, wikilinks) - 索引服务 (增量更新, 重建) - MCP 工具集成测试 Co-Authored-By: Claude Dev <noreply@anthropic.com>
380 lines
11 KiB
Python
380 lines
11 KiB
Python
"""
|
|
Unit tests for Service Layer
|
|
|
|
测试缓存、查询、解析和索引服务。
|
|
"""
|
|
|
|
import pytest
|
|
import asyncio
|
|
import tempfile
|
|
import os
|
|
from datetime import datetime
|
|
|
|
from mcp_server.services import (
|
|
CacheService,
|
|
QueryService,
|
|
ParserService,
|
|
IndexerService
|
|
)
|
|
from mcp_server.storage import Database, WikiPage
|
|
|
|
|
|
@pytest.fixture
|
|
async def db():
|
|
"""创建测试数据库"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = os.path.join(tmpdir, "test.db")
|
|
db = Database(db_path)
|
|
await db.connect()
|
|
yield db
|
|
await db.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def cache():
|
|
"""创建缓存服务"""
|
|
return CacheService(max_size=10)
|
|
|
|
|
|
@pytest.fixture
|
|
def parser():
|
|
"""创建解析服务"""
|
|
return ParserService()
|
|
|
|
|
|
class TestCacheService:
|
|
"""缓存服务测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_and_get(self, cache):
|
|
"""测试设置和获取缓存"""
|
|
await cache.set("key1", "value1")
|
|
value = await cache.get("key1")
|
|
assert value == "value1"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cache_miss(self, cache):
|
|
"""测试缓存未命中"""
|
|
value = await cache.get("nonexistent")
|
|
assert value is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ttl_expiration(self, cache):
|
|
"""测试 TTL 过期"""
|
|
await cache.set("key", "value", ttl=1)
|
|
# 立即获取应该成功
|
|
assert await cache.get("key") == "value"
|
|
# 等待过期后应该返回 None(实际测试中可能需要调整时间)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lru_eviction(self, cache):
|
|
"""测试 LRU 淘汰策略"""
|
|
# 填满缓存
|
|
for i in range(10):
|
|
await cache.set(f"key{i}", f"value{i}")
|
|
|
|
# 添加第 11 个应该淘汰最旧的
|
|
await cache.set("key10", "value10")
|
|
assert await cache.get("key0") is None
|
|
assert await cache.get("key10") == "value10"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalidate(self, cache):
|
|
"""测试缓存失效"""
|
|
await cache.set("key1", "value1")
|
|
await cache.set("key2", "value2")
|
|
|
|
count = await cache.invalidate("key1")
|
|
assert count == 1
|
|
assert await cache.get("key1") is None
|
|
assert await cache.get("key2") == "value2"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalidate_all(self, cache):
|
|
"""测试清空所有缓存"""
|
|
await cache.set("key1", "value1")
|
|
await cache.set("key2", "value2")
|
|
|
|
count = await cache.invalidate("*")
|
|
assert count == 2
|
|
assert await cache.get("key1") is None
|
|
assert await cache.get("key2") is None
|
|
|
|
|
|
class TestParserService:
|
|
"""解析服务测试"""
|
|
|
|
def test_parse_frontmatter(self, parser):
|
|
"""测试 frontmatter 解析"""
|
|
content = """---
|
|
name: test-page
|
|
description: Test description
|
|
metadata:
|
|
type: practice
|
|
tags: [tag1, tag2]
|
|
---
|
|
# Page Content
|
|
"""
|
|
frontmatter = parser.parse_frontmatter(content)
|
|
assert frontmatter["name"] == "test-page"
|
|
assert frontmatter["description"] == "Test description"
|
|
assert frontmatter["metadata"]["type"] == "practice"
|
|
|
|
def test_parse_frontmatter_with_yaml(self, parser):
|
|
"""测试使用 PyYAML 解析复杂 frontmatter"""
|
|
content = """---
|
|
name: complex-page
|
|
description: Multi-line description
|
|
with multiple lines
|
|
tags:
|
|
- tag1
|
|
- tag2
|
|
- tag3
|
|
metadata:
|
|
type: concept
|
|
nested:
|
|
key: value
|
|
---
|
|
Content here
|
|
"""
|
|
frontmatter = parser.parse_frontmatter(content)
|
|
assert frontmatter["name"] == "complex-page"
|
|
assert isinstance(frontmatter["tags"], list)
|
|
assert len(frontmatter["tags"]) == 3
|
|
assert "Multi-line description" in frontmatter["description"]
|
|
|
|
def test_extract_links(self, parser):
|
|
"""测试提取 wikilinks"""
|
|
content = """
|
|
This is a page with [[link1]] and [[link2|alias]].
|
|
Also [[link3]] here.
|
|
"""
|
|
links = parser.extract_links(content)
|
|
assert "link1" in links
|
|
assert "link2" in links
|
|
assert "link3" in links
|
|
|
|
def test_extract_tags(self, parser):
|
|
"""测试提取标签"""
|
|
content = """---
|
|
tags: [python, async, testing]
|
|
---
|
|
Some content
|
|
"""
|
|
tags = parser.extract_tags(content)
|
|
assert "python" in tags
|
|
assert "async" in tags
|
|
assert "testing" in tags
|
|
|
|
def test_extract_title(self, parser):
|
|
"""测试提取标题"""
|
|
# 从 frontmatter 提取
|
|
content1 = """---
|
|
title: Frontmatter Title
|
|
---
|
|
Content
|
|
"""
|
|
assert parser.extract_title(content1) == "Frontmatter Title"
|
|
|
|
# 从第一个 # 标题提取
|
|
content2 = """# Heading Title
|
|
|
|
Some content
|
|
"""
|
|
assert parser.extract_title(content2) == "Heading Title"
|
|
|
|
def test_extract_summary(self, parser):
|
|
"""测试提取摘要"""
|
|
content = """---
|
|
description: Frontmatter summary
|
|
---
|
|
|
|
Content here
|
|
"""
|
|
summary = parser.extract_summary(content)
|
|
assert summary == "Frontmatter summary"
|
|
|
|
def test_infer_category(self, parser):
|
|
"""测试推断分类"""
|
|
assert parser.infer_category("practices/test.md") == "practices"
|
|
assert parser.infer_category("concepts/test.md") == "concepts"
|
|
assert parser.infer_category("root.md") == "uncategorized"
|
|
|
|
def test_validate_page(self, parser):
|
|
"""测试页面验证"""
|
|
valid_content = """---
|
|
title: Valid Page
|
|
---
|
|
# Valid Page
|
|
|
|
Some content with proper structure.
|
|
"""
|
|
issues = parser.validate_page("test/valid.md", valid_content)
|
|
assert len(issues) == 0 # 应该没有问题
|
|
|
|
invalid_content = "No title or proper structure"
|
|
issues = parser.validate_page("test/invalid.md", invalid_content)
|
|
assert len(issues) > 0 # 应该有问题
|
|
|
|
def test_parse_wiki_page(self, parser):
|
|
"""测试完整解析 wiki 页面"""
|
|
content = """---
|
|
title: Test Page
|
|
tags: [test]
|
|
---
|
|
# Test Page
|
|
|
|
Content here
|
|
"""
|
|
parsed = parser.parse_wiki_page("test/page.md", content)
|
|
assert parsed["path"] == "test/page.md"
|
|
assert parsed["title"] == "Test Page"
|
|
assert parsed["category"] == "test"
|
|
assert "test" in parsed["tags"]
|
|
|
|
|
|
class TestQueryService:
|
|
"""查询服务测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_with_cache(self, db, cache):
|
|
"""测试带缓存的查询"""
|
|
query_service = QueryService(db, cache)
|
|
|
|
# 添加测试数据
|
|
await db.upsert_page(WikiPage(
|
|
path="test/page.md",
|
|
title="Test Page",
|
|
category="test",
|
|
tags=["test"],
|
|
summary="Test summary",
|
|
content_hash="hash",
|
|
lifecycle="draft",
|
|
source_tool="claude",
|
|
created_at="2024-01-01T00:00:00",
|
|
updated_at="2024-01-01T00:00:00",
|
|
indexed_at="2024-01-01T00:00:00"
|
|
))
|
|
|
|
# 第一次查询
|
|
result1 = await query_service.get_page("test/page.md")
|
|
assert result1 is not None
|
|
|
|
# 第二次查询应该从缓存获取
|
|
result2 = await query_service.get_page("test/page.md")
|
|
assert result2 is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cache_invalidation(self, db, cache):
|
|
"""测试缓存失效"""
|
|
query_service = QueryService(db, cache)
|
|
|
|
await db.upsert_page(WikiPage(
|
|
path="test/page.md",
|
|
title="Original",
|
|
category="test",
|
|
tags=[],
|
|
summary="Original",
|
|
content_hash="hash1",
|
|
lifecycle="draft",
|
|
source_tool="claude",
|
|
created_at="2024-01-01T00:00:00",
|
|
updated_at="2024-01-01T00:00:00",
|
|
indexed_at="2024-01-01T00:00:00"
|
|
))
|
|
|
|
# 第一次查询
|
|
result1 = await query_service.get_page("test/page.md")
|
|
assert result1.title == "Original"
|
|
|
|
# 更新页面
|
|
await db.upsert_page(WikiPage(
|
|
path="test/page.md",
|
|
title="Updated",
|
|
category="test",
|
|
tags=[],
|
|
summary="Updated",
|
|
content_hash="hash2",
|
|
lifecycle="draft",
|
|
source_tool="claude",
|
|
created_at="2024-01-01T00:00:00",
|
|
updated_at="2024-01-02T00:00:00",
|
|
indexed_at="2024-01-02T00:00:00"
|
|
))
|
|
|
|
# 失效缓存
|
|
await query_service.invalidate_cache("test/page.md")
|
|
|
|
# 重新查询应该获取更新后的数据
|
|
result2 = await query_service.get_page("test/page.md")
|
|
assert result2.title == "Updated"
|
|
|
|
|
|
class TestIndexerService:
|
|
"""索引服务测试"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_index_page(self, db, parser):
|
|
"""测试索引单个页面"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# 创建测试文件
|
|
test_file = os.path.join(tmpdir, "test.md")
|
|
with open(test_file, 'w') as f:
|
|
f.write("""---
|
|
title: Test Page
|
|
tags: [test]
|
|
---
|
|
# Test Page
|
|
|
|
Content here
|
|
""")
|
|
|
|
indexer = IndexerService(db, parser, tmpdir)
|
|
page = await indexer.index_page("test.md")
|
|
|
|
assert page is not None
|
|
assert page.title == "Test Page"
|
|
|
|
# 验证数据库中存在
|
|
retrieved = await db.get_page("test.md")
|
|
assert retrieved is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_incremental_update(self, db, parser):
|
|
"""测试增量更新"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# 创建测试文件
|
|
test_file = os.path.join(tmpdir, "test.md")
|
|
with open(test_file, 'w') as f:
|
|
f.write("# Test")
|
|
|
|
indexer = IndexerService(db, parser, tmpdir)
|
|
|
|
# 首次索引
|
|
await indexer.incremental_update()
|
|
|
|
# 修改文件
|
|
with open(test_file, 'w') as f:
|
|
f.write("# Updated Test")
|
|
|
|
# 再次增量更新
|
|
stats = await indexer.incremental_update()
|
|
assert stats is not None
|
|
|
|
def test_scan_wiki_vault(self, db, parser):
|
|
"""测试扫描 wiki vault"""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# 创建测试文件结构
|
|
os.makedirs(os.path.join(tmpdir, "practices"), exist_ok=True)
|
|
with open(os.path.join(tmpdir, "practices", "test.md"), 'w') as f:
|
|
f.write("# Test")
|
|
with open(os.path.join(tmpdir, "index.md"), 'w') as f:
|
|
f.write("# Index")
|
|
|
|
indexer = IndexerService(db, parser, tmpdir)
|
|
files = indexer.scan_wiki_vault()
|
|
|
|
assert len(files) == 2
|
|
assert "practices/test.md" in files
|
|
assert "index.md" in files
|