Files
claude_dev 421b3ada3b feat: 搜索改进与代码修复
- 连字符扩展:multi-agent → multi agent 提升搜索召回率
- 中文同义词支持:添加中英文混合查询扩展
- Snippet 高亮预留:为 FTS5 snippet 功能预留接口
- YAML 解析增强:支持 HTML 实体解码
- 标签关联维护:自动维护 wiki_page_tags 关联表
- 缓存失效优化:支持智能前缀匹配
- 设计文档更新:汇总近期改动 (v1.3)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:47:28 +08:00

381 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