""" Unit tests for Database (Storage Layer) 测试数据库层的 CRUD 操作和 FTS5 搜索功能。 """ import pytest import asyncio import tempfile import os from pathlib import Path from datetime import datetime from mcp_server.storage import Database, WikiPage, compute_content_hash @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 sample_page(): """示例 WikiPage 对象""" return WikiPage( path="test/page.md", title="Test Page", category="test", tags=["tag1", "tag2"], summary="Test summary", content_hash="abc123", 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" ) class TestDatabaseBasics: """基础数据库操作测试""" @pytest.mark.asyncio async def test_connect_and_close(self, db): """测试数据库连接和关闭""" assert db._conn is not None await db.close() assert db._conn is None @pytest.mark.asyncio async def test_upsert_and_get_page(self, db, sample_page): """测试页面插入和获取""" await db.upsert_page(sample_page) retrieved = await db.get_page("test/page.md") assert retrieved is not None assert retrieved.path == "test/page.md" assert retrieved.title == "Test Page" assert retrieved.category == "test" assert retrieved.tags == ["tag1", "tag2"] @pytest.mark.asyncio async def test_update_existing_page(self, db, sample_page): """测试更新已存在页面""" await db.upsert_page(sample_page) # 更新页面 updated_page = WikiPage( path="test/page.md", title="Updated Title", category="test", tags=["tag1", "tag2", "tag3"], summary="Updated summary", content_hash="xyz789", lifecycle="verified", 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 db.upsert_page(updated_page) retrieved = await db.get_page("test/page.md") assert retrieved.title == "Updated Title" assert retrieved.tags == ["tag1", "tag2", "tag3"] assert retrieved.lifecycle == "verified" @pytest.mark.asyncio async def test_delete_page(self, db, sample_page): """测试删除页面""" await db.upsert_page(sample_page) assert await db.get_page("test/page.md") is not None await db.delete_page("test/page.md") assert await db.get_page("test/page.md") is None @pytest.mark.asyncio async def test_get_all_pages(self, db): """测试获取所有页面""" pages = [ WikiPage( path=f"test/page{i}.md", title=f"Page {i}", category="test", tags=[f"tag{i}"], summary=f"Summary {i}", content_hash=f"hash{i}", 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" ) for i in range(5) ] for page in pages: await db.upsert_page(page) all_pages = await db.get_all_pages() assert len(all_pages) == 5 class TestDatabaseSearch: """搜索功能测试""" @pytest.mark.asyncio async def test_fts_search(self, db): """测试 FTS5 全文搜索""" # 创建测试页面 await db.upsert_page(WikiPage( path="python/basics.md", title="Python Basics", category="concepts", tags=["python", "basics"], summary="Python programming language basics", 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" )) await db.update_fts_content( "python/basics.md", "Python Basics", "Python is a high-level programming language.", "Python programming language basics" ) await db.upsert_page(WikiPage( path="javascript/basics.md", title="JavaScript Basics", category="concepts", tags=["javascript", "basics"], summary="JavaScript programming language basics", content_hash="hash2", 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" )) await db.update_fts_content( "javascript/basics.md", "JavaScript Basics", "JavaScript is a scripting language for the web.", "JavaScript programming language basics" ) # 测试搜索 results = await db.fts_search("Python", limit=10) assert len(results) == 1 assert results[0].path == "python/basics.md" results = await db.fts_search("programming", limit=10) assert len(results) == 2 @pytest.mark.asyncio async def test_fts_search_validation(self, db): """测试 FTS5 查询验证""" # 空查询 assert await db.fts_search("") == [] # 过长查询(应被截断) long_query = "a" * 1000 results = await db.fts_search(long_query) assert results == [] # 无匹配结果 @pytest.mark.asyncio async def test_search_by_tags(self, db): """测试按标签搜索""" await db.upsert_page(WikiPage( path="test/page1.md", title="Page 1", category="test", tags=["python", "async"], summary="Summary", 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" )) await db.upsert_page(WikiPage( path="test/page2.md", title="Page 2", category="test", tags=["python", "sync"], summary="Summary", content_hash="hash2", 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" )) results = await db.search_by_tags(["python"]) assert len(results) == 2 results = await db.search_by_tags(["async"]) assert len(results) == 1 assert results[0].path == "test/page1.md" @pytest.mark.asyncio async def test_search_by_source_tool(self, db): """测试按来源工具搜索""" await db.upsert_page(WikiPage( path="claude/page.md", title="Claude Page", category="test", tags=["claude"], summary="Summary", 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" )) await db.upsert_page(WikiPage( path="gitea/page.md", title="Gitea Page", category="test", tags=["gitea"], summary="Summary", content_hash="hash2", lifecycle="draft", source_tool="gitea", created_at="2024-01-01T00:00:00", updated_at="2024-01-01T00:00:00", indexed_at="2024-01-01T00:00:00" )) results = await db.search_by_source_tool("claude") assert len(results) == 1 assert results[0].source_tool == "claude" class TestDatabaseLinks: """链接关系测试""" @pytest.mark.asyncio async def test_upsert_and_get_links(self, db): """测试链接关系""" await db.upsert_link("source.md", "target1.md") await db.upsert_link("source.md", "target2.md") links = await db.get_links("source.md") assert links == {"target1.md", "target2.md"} @pytest.mark.asyncio async def test_backlinks(self, db): """测试反向链接""" await db.upsert_link("page1.md", "target.md") await db.upsert_link("page2.md", "target.md") backlinks = await db.get_backlinks("target.md") assert backlinks == {"page1.md", "page2.md"} @pytest.mark.asyncio async def test_delete_links(self, db): """测试删除链接""" await db.upsert_link("source.md", "target1.md") await db.upsert_link("source.md", "target2.md") await db.delete_links("source.md") links = await db.get_links("source.md") assert links == set() class TestDatabaseTags: """标签索引测试""" @pytest.mark.asyncio async def test_upsert_and_get_tags(self, db): """测试标签操作""" await db.upsert_tag("python", 5) await db.upsert_tag("async", 3) tags = await db.get_all_tags() assert tags == {"python": 5, "async": 3} @pytest.mark.asyncio async def test_increment_tag_count(self, db): """测试标签计数增加""" await db.upsert_tag("python", 5) await db.upsert_tag("python", 3) # 应该累加 tags = await db.get_all_tags() assert tags["python"] == 8 class TestDatabaseMeta: """元数据测试""" @pytest.mark.asyncio async def test_set_and_get_meta(self, db): """测试元数据操作""" await db.set_meta("version", "1.0.0") await db.set_meta("last_update", "2024-01-15") assert await db.get_meta("version") == "1.0.0" assert await db.get_meta("last_update") == "2024-01-15" assert await db.get_meta("nonexistent") == "" class TestDatabaseStats: """统计功能测试""" @pytest.mark.asyncio async def test_get_stats(self, db): """测试获取统计信息""" await db.upsert_page(WikiPage( path="test/page.md", title="Test", category="test", tags=["tag1"], summary="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" )) await db.upsert_link("test/page.md", "other.md") await db.upsert_tag("tag1", 1) stats = await db.get_stats() assert stats["total_pages"] == 1 assert stats["total_links"] == 1 assert stats["total_tags"] == 1 assert stats["last_indexed"] is not None @pytest.mark.asyncio async def test_get_recent_pages(self, db): """测试获取最近更新的页面""" await db.upsert_page(WikiPage( path="old/page.md", title="Old", category="test", tags=[], summary="Old", 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" )) await db.upsert_page(WikiPage( path="recent/page.md", title="Recent", category="test", tags=[], summary="Recent", content_hash="hash2", lifecycle="draft", source_tool="claude", created_at="2024-01-10T00:00:00", updated_at="2024-01-10T00:00:00", indexed_at="2024-01-10T00:00:00" )) recent = await db.get_recent_pages(days=7, limit=10) # 注意:这取决于当前日期,可能需要调整 assert isinstance(recent, list) class TestDatabaseIntegrity: """完整性检查测试""" @pytest.mark.asyncio async def test_check_integrity(self, db): """测试完整性检查""" result = await db.check_integrity() assert result is True class TestUtilities: """工具函数测试""" def test_compute_content_hash(self): """测试内容哈希计算""" content1 = "Hello, World!" content2 = "Hello, World!" content3 = "Different content" hash1 = compute_content_hash(content1) hash2 = compute_content_hash(content2) hash3 = compute_content_hash(content3) assert hash1 == hash2 # 相同内容应产生相同哈希 assert hash1 != hash3 # 不同内容应产生不同哈希 assert len(hash1) == 32 # MD5 哈希长度 class TestWikiPageModel: """WikiPage 模型测试""" def test_is_stale(self): """测试过期检查""" old_page = WikiPage( path="old.md", title="Old", category="test", tags=[], summary="Old", content_hash="hash", lifecycle="draft", source_tool="claude", created_at="2020-01-01T00:00:00", updated_at="2020-01-01T00:00:00", indexed_at="2024-01-01T00:00:00" ) # 这个测试取决于当前日期,可能需要调整 # assert old_page.is_stale(days=100) == True def test_to_dict(self): """测试转换为字典""" page = WikiPage( path="test.md", title="Test", category="test", tags=["tag1"], summary="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" ) d = page.to_dict() assert d["path"] == "test.md" assert d["title"] == "Test" assert d["tags"] == ["tag1"]