diff --git a/tests/conftest.py b/tests/conftest.py index d31b0939..e2d6c658 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ from basic_memory import db from basic_memory.config import ProjectConfig from basic_memory.db import DatabaseType from basic_memory.markdown.knowledge_writer import KnowledgeWriter +from basic_memory.markdown.knowledge_parser import KnowledgeParser from basic_memory.models import Base from basic_memory.models.knowledge import Entity from basic_memory.repository.document_repository import DocumentRepository @@ -22,10 +23,10 @@ from basic_memory.services import ( ObservationService, RelationService, DocumentService, - KnowledgeService, FileChangeScanner, - FileService ) +from basic_memory.services.file_service import FileService +from basic_memory.services import KnowledgeService @pytest_asyncio.fixture @@ -67,7 +68,6 @@ async def session_maker(engine_factory) -> async_sessionmaker[AsyncSession]: return session_maker - @pytest_asyncio.fixture(scope="function") async def document_repository( session_maker: async_sessionmaker[AsyncSession], @@ -140,8 +140,14 @@ def knowledge_writer(): return KnowledgeWriter() +@pytest.fixture +def knowledge_parser(): + """Create parser instance.""" + return KnowledgeParser() + + @pytest_asyncio.fixture -def file_change_scanner(document_repository, entity_repository) -> FileChangeScanner: +def file_sync_service(document_repository, entity_repository) -> FileChangeScanner: """Create FileChangeScanner instance.""" return FileChangeScanner(document_repository, entity_repository) @@ -176,4 +182,4 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity: "path_id": "test/test_entity", "file_path": "test/test_entity.md", } - return await entity_repository.create(entity_data) + return await entity_repository.create(entity_data) \ No newline at end of file diff --git a/tests/services/test_document_sync_service.py b/tests/services/sync/test_document_sync_service.py similarity index 100% rename from tests/services/test_document_sync_service.py rename to tests/services/sync/test_document_sync_service.py diff --git a/tests/services/sync/test_knowledge_sync_service.py b/tests/services/sync/test_knowledge_sync_service.py new file mode 100644 index 00000000..3ac21bf7 --- /dev/null +++ b/tests/services/sync/test_knowledge_sync_service.py @@ -0,0 +1,184 @@ +"""Tests for knowledge sync service.""" + +import pytest +import pytest_asyncio +from pathlib import Path +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession + +from basic_memory.services.sync.knowledge_sync_service import KnowledgeSyncService +from basic_memory.services import KnowledgeService, FileChangeScanner +from basic_memory.markdown import KnowledgeParser +from basic_memory.utils.file_utils import compute_checksum + + +@pytest_asyncio.fixture +async def knowledge_sync_service( + file_sync_service: FileChangeScanner, # from conftest + knowledge_service: KnowledgeService, # from conftest + knowledge_parser: KnowledgeParser # need to add to conftest +) -> KnowledgeSyncService: + """Create knowledge sync service for testing.""" + return KnowledgeSyncService(file_sync_service, knowledge_service, knowledge_parser) + + +async def create_test_entity_file( + path: Path, + entity_type: str = "test", + name: str = "Test Entity", + content: str = "test content" +) -> None: + """Create a test entity file with frontmatter.""" + path.parent.mkdir(parents=True, exist_ok=True) + + # Create basic entity markdown + file_content = f"""--- +type: {entity_type} +id: {entity_type}/{name.lower().replace(' ', '_')} +created: 2024-12-27T10:00:00Z +modified: 2024-12-27T10:00:00Z +tags: [test] +--- + +# {name} + +{content} + +## Observations +- [test] First observation #test +- [test] Second observation #test + +## Relations +- relates_to [[Other_Entity]] +""" + path.write_text(file_content) + + +@pytest.mark.asyncio +async def test_sync_new_entity( + knowledge_sync_service: KnowledgeSyncService, + test_config, +): + """Test syncing a new entity file.""" + knowledge_dir = test_config.knowledge_dir + test_file = knowledge_dir / "test" / "test_entity.md" + + # Create test file + await create_test_entity_file(test_file) + + # Run sync + changes = await knowledge_sync_service.sync(knowledge_dir) + + # Verify changes + assert len(changes.new) == 1 + assert "test/test_entity.md" in changes.new + + # Verify entity in DB + entity = await knowledge_sync_service.knowledge_service.get_entity_by_path_id("test/test_entity") + assert entity.name == "Test Entity" + assert entity.entity_type == "test" + assert len(entity.observations) == 2 + assert len(entity.outbound_relations) == 1 + + +@pytest.mark.asyncio +async def test_sync_modified_entity( + knowledge_sync_service: KnowledgeSyncService, + test_config, + entity_repository, +): + """Test syncing a modified entity file.""" + knowledge_dir = test_config.knowledge_dir + test_file = knowledge_dir / "test" / "test_entity.md" + + # Create initial file and sync + await create_test_entity_file(test_file) + await knowledge_sync_service.sync(knowledge_dir) + + # Modify file + await create_test_entity_file( + test_file, + content="modified content", + name="Test Entity" # Keep same name/path + ) + + # Run sync again + changes = await knowledge_sync_service.sync(knowledge_dir) + + # Verify changes + assert len(changes.modified) == 1 + assert "test/test_entity.md" in changes.modified + + # Verify entity updated + entity = await knowledge_sync_service.knowledge_service.get_entity_by_path_id("test/test_entity") + assert "modified content" in entity.description + + +@pytest.mark.asyncio +async def test_sync_moved_entity( + knowledge_sync_service: KnowledgeSyncService, + test_config, + entity_repository +): + """Test syncing a moved entity file.""" + knowledge_dir = test_config.knowledge_dir + original_file = knowledge_dir / "test" / "original.md" + new_file = knowledge_dir / "test" / "moved.md" + + # Create initial file and sync + await create_test_entity_file(original_file, name="Original") + await knowledge_sync_service.sync(knowledge_dir) + + # Create new file with same content + new_file.parent.mkdir(parents=True, exist_ok=True) + await create_test_entity_file(new_file, name="Moved") + original_file.unlink() + + # Run sync + changes = await knowledge_sync_service.sync(knowledge_dir) + + # Verify changes + assert len(changes.moved) == 1 + assert "test/moved.md" in changes.moved + assert changes.moved["test/moved.md"].moved_from == "test/original.md" + + # Verify entity updated + with pytest.raises(Exception): + # Original should be gone + await knowledge_sync_service.knowledge_service.get_entity_by_path_id("test/original") + + # New entity should exist + entity = await knowledge_sync_service.knowledge_service.get_entity_by_path_id("test/moved") + assert entity.name == "Moved" + assert len(entity.observations) == 2 + assert len(entity.outbound_relations) == 1 + + +@pytest.mark.asyncio +async def test_sync_deleted_entity( + knowledge_sync_service: KnowledgeSyncService, + test_config, + entity_repository +): + """Test syncing a deleted entity file.""" + knowledge_dir = test_config.knowledge_dir + test_file = knowledge_dir / "test" / "to_delete.md" + + # Create initial file and sync + await create_test_entity_file(test_file) + await knowledge_sync_service.sync(knowledge_dir) + + # Delete file + test_file.unlink() + + # Run sync + changes = await knowledge_sync_service.sync(knowledge_dir) + + # Verify changes + assert len(changes.deleted) == 1 + assert "test/to_delete.md" in changes.deleted + + # Verify entity deleted + with pytest.raises(Exception): + await knowledge_sync_service.knowledge_service.get_entity_by_path_id("test/to_delete") \ No newline at end of file