""" 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