Skip to main content
Glama
test_character_conflicts.py12.8 kB
"""Test character-related sync conflicts and permalink generation.""" from pathlib import Path from textwrap import dedent import pytest from sqlalchemy.exc import IntegrityError from basic_memory.config import ProjectConfig from basic_memory.repository import EntityRepository from basic_memory.sync.sync_service import SyncService from basic_memory.utils import ( generate_permalink, normalize_file_path_for_comparison, detect_potential_file_conflicts, ) async def create_test_file(path: Path, content: str = "test content") -> None: """Create a test file with given content.""" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content) class TestUtilityFunctions: """Test utility functions for file path normalization and conflict detection.""" def test_normalize_file_path_for_comparison(self): """Test file path normalization for conflict detection.""" # Case sensitivity normalization assert ( normalize_file_path_for_comparison("Finance/Investment.md") == "finance/investment.md" ) assert ( normalize_file_path_for_comparison("FINANCE/INVESTMENT.MD") == "finance/investment.md" ) # Path separator normalization assert ( normalize_file_path_for_comparison("Finance\\Investment.md") == "finance/investment.md" ) # Multiple slash handling assert ( normalize_file_path_for_comparison("Finance//Investment.md") == "finance/investment.md" ) def test_detect_potential_file_conflicts(self): """Test the enhanced conflict detection function.""" existing_paths = [ "Finance/Investment.md", "finance/Investment.md", "docs/my-feature.md", "docs/my feature.md", ] # Case sensitivity conflict conflicts = detect_potential_file_conflicts("FINANCE/INVESTMENT.md", existing_paths) assert "Finance/Investment.md" in conflicts assert "finance/Investment.md" in conflicts # Permalink conflict (space vs hyphen) conflicts = detect_potential_file_conflicts("docs/my_feature.md", existing_paths) assert "docs/my-feature.md" in conflicts assert "docs/my feature.md" in conflicts class TestPermalinkGeneration: """Test permalink generation with various character scenarios.""" def test_hyphen_handling(self): """Test that hyphens in filenames are handled consistently.""" # File with existing hyphens assert generate_permalink("docs/my-feature.md") == "docs/my-feature" assert generate_permalink("docs/basic-memory bug.md") == "docs/basic-memory-bug" # File with spaces that become hyphens assert generate_permalink("docs/my feature.md") == "docs/my-feature" # Mixed scenarios assert generate_permalink("docs/my-old feature.md") == "docs/my-old-feature" def test_forward_slash_handling(self): """Test that forward slashes are handled properly.""" # Normal directory structure assert generate_permalink("Finance/Investment.md") == "finance/investment" # Path with spaces in directory names assert generate_permalink("My Finance/Investment.md") == "my-finance/investment" def test_case_sensitivity_normalization(self): """Test that case differences are normalized consistently.""" # Same logical path with different cases assert generate_permalink("Finance/Investment.md") == "finance/investment" assert generate_permalink("finance/Investment.md") == "finance/investment" assert generate_permalink("FINANCE/INVESTMENT.md") == "finance/investment" def test_unicode_character_handling(self): """Test that international characters are handled properly.""" # Italian characters as mentioned in user feedback assert ( generate_permalink("Finance/Punti Chiave di Peter Lynch.md") == "finance/punti-chiave-di-peter-lynch" ) # Chinese characters (should be preserved) assert generate_permalink("中文/测试文档.md") == "中文/测试文档" # Mixed international characters assert generate_permalink("docs/Café München.md") == "docs/cafe-munchen" def test_special_punctuation(self): """Test handling of special punctuation characters.""" # Apostrophes should be removed assert generate_permalink("Peter's Guide.md") == "peters-guide" # Other punctuation should become hyphens assert generate_permalink("Q&A Session.md") == "q-a-session" @pytest.mark.asyncio class TestSyncConflictHandling: """Test sync service handling of file path and permalink conflicts.""" async def test_file_path_conflict_detection( self, sync_service: SyncService, project_config: ProjectConfig, entity_repository: EntityRepository, ): """Test that file path conflicts are detected during move operations.""" project_dir = project_config.home # Create two files content1 = dedent(""" --- type: knowledge --- # Document One This is the first document. """) content2 = dedent(""" --- type: knowledge --- # Document Two This is the second document. """) await create_test_file(project_dir / "doc1.md", content1) await create_test_file(project_dir / "doc2.md", content2) # Initial sync await sync_service.sync(project_config.home) # Verify both entities exist entities = await entity_repository.find_all() assert len(entities) == 2 # Now simulate a move where doc1.md tries to move to doc2.md's location # This should be handled gracefully, not throw an IntegrityError # First, get the entities entity1 = await entity_repository.get_by_file_path("doc1.md") entity2 = await entity_repository.get_by_file_path("doc2.md") assert entity1 is not None assert entity2 is not None # Simulate the conflict scenario with pytest.raises(Exception) as exc_info: # This should detect the conflict and handle it gracefully await sync_service.handle_move("doc1.md", "doc2.md") # The exception should be a meaningful error, not an IntegrityError assert not isinstance(exc_info.value, IntegrityError) async def test_hyphen_filename_conflict( self, sync_service: SyncService, project_config: ProjectConfig, entity_repository: EntityRepository, ): """Test conflict when filename with hyphens conflicts with generated permalink.""" project_dir = project_config.home # Create file with spaces (will generate permalink with hyphens) content1 = dedent(""" --- type: knowledge --- # Basic Memory Bug This file has spaces in the name. """) # Create file with hyphens (already has hyphens in filename) content2 = dedent(""" --- type: knowledge --- # Basic Memory Bug Report This file has hyphens in the name. """) await create_test_file(project_dir / "basic memory bug.md", content1) await create_test_file(project_dir / "basic-memory-bug.md", content2) # Sync should handle this without conflict await sync_service.sync(project_config.home) # Verify both entities were created with unique permalinks entities = await entity_repository.find_all() assert len(entities) == 2 # Check that permalinks are unique permalinks = [entity.permalink for entity in entities if entity.permalink] assert len(set(permalinks)) == len(permalinks), "Permalinks should be unique" async def test_case_sensitivity_conflict( self, sync_service: SyncService, project_config: ProjectConfig, entity_repository: EntityRepository, ): """Test conflict handling when case differences cause issues.""" import platform project_dir = project_config.home # Create directory structure that might cause case conflicts (project_dir / "Finance").mkdir(parents=True, exist_ok=True) (project_dir / "finance").mkdir(parents=True, exist_ok=True) content1 = dedent(""" --- type: knowledge --- # Investment Guide Upper case directory. """) content2 = dedent(""" --- type: knowledge --- # Investment Tips Lower case directory. """) await create_test_file(project_dir / "Finance" / "investment.md", content1) await create_test_file(project_dir / "finance" / "investment.md", content2) # Sync should handle case differences properly await sync_service.sync(project_config.home) # Verify entities were created entities = await entity_repository.find_all() # On case-insensitive file systems (macOS, Windows), only one entity will be created # On case-sensitive file systems (Linux), two entities will be created if platform.system() in ["Darwin", "Windows"]: # Case-insensitive file systems assert len(entities) >= 1 # Only one of the paths will exist file_paths = [entity.file_path for entity in entities] assert any( path in ["Finance/investment.md", "finance/investment.md"] for path in file_paths ) else: # Case-sensitive file systems (Linux) assert len(entities) >= 2 # Check that file paths are preserved correctly file_paths = [entity.file_path for entity in entities] assert "Finance/investment.md" in file_paths assert "finance/investment.md" in file_paths async def test_move_conflict_resolution( self, sync_service: SyncService, project_config: ProjectConfig, entity_repository: EntityRepository, ): """Test that move conflicts are resolved with proper error handling.""" project_dir = project_config.home # Create three files in a scenario that could cause move conflicts await create_test_file(project_dir / "file-a.md", "# File A") await create_test_file(project_dir / "file-b.md", "# File B") await create_test_file(project_dir / "temp.md", "# Temp File") # Initial sync await sync_service.sync(project_config.home) # Simulate a complex move scenario where files swap locations # This is the kind of scenario that caused the original bug # Get the entities entity_a = await entity_repository.get_by_file_path("file-a.md") entity_b = await entity_repository.get_by_file_path("file-b.md") entity_temp = await entity_repository.get_by_file_path("temp.md") assert all([entity_a, entity_b, entity_temp]) # Try to move file-a to file-b's location (should detect conflict) try: await sync_service.handle_move("file-a.md", "file-b.md") # If this doesn't raise an exception, the conflict was resolved # Verify the state is consistent updated_entities = await entity_repository.find_all() file_paths = [entity.file_path for entity in updated_entities] # Should not have duplicate file paths assert len(file_paths) == len(set(file_paths)), "File paths should be unique" except Exception as e: # If an exception is raised, it should be a meaningful error assert "conflict" in str(e).lower() or "already exists" in str(e).lower() assert not isinstance(e, IntegrityError), "Should not be a raw IntegrityError" @pytest.mark.asyncio class TestEnhancedErrorMessages: """Test that error messages provide helpful guidance for character conflicts.""" async def test_helpful_error_for_hyphen_conflict( self, sync_service: SyncService, project_config: ProjectConfig, ): """Test that hyphen conflicts generate helpful error messages.""" # This test will be implemented after we enhance the error handling pass async def test_helpful_error_for_case_conflict( self, sync_service: SyncService, project_config: ProjectConfig, ): """Test that case sensitivity conflicts generate helpful error messages.""" # This test will be implemented after we enhance the error handling pass

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/basicmachines-co/basic-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server