From 43fa5762a8a85eee0087fe74aadc9191113fb813 Mon Sep 17 00:00:00 2001 From: Drew Cain Date: Fri, 1 Aug 2025 21:50:18 -0500 Subject: [PATCH] ruff checks Signed-off-by: Drew Cain --- .../api/routers/project_router.py | 2 +- src/basic_memory/cli/commands/project.py | 34 +++-- .../importers/chatgpt_importer.py | 10 +- .../repository/project_repository.py | 4 +- src/basic_memory/services/entity_service.py | 22 +-- src/basic_memory/services/project_service.py | 14 +- src/basic_memory/sync/sync_service.py | 2 +- src/basic_memory/utils.py | 38 +++--- tests/api/test_project_router.py | 104 +++++++------- tests/repository/test_project_repository.py | 6 +- tests/services/test_project_service.py | 50 ++++--- tests/sync/test_character_conflicts.py | 128 ++++++++++-------- 12 files changed, 212 insertions(+), 202 deletions(-) diff --git a/src/basic_memory/api/routers/project_router.py b/src/basic_memory/api/routers/project_router.py index bbfc11c3..e0118353 100644 --- a/src/basic_memory/api/routers/project_router.py +++ b/src/basic_memory/api/routers/project_router.py @@ -51,7 +51,7 @@ async def update_project( # Validate that path is absolute if provided if path and not os.path.isabs(path): raise HTTPException(status_code=400, detail="Path must be absolute") - + # Get original project info for the response old_project_info = ProjectItem( name=name, diff --git a/src/basic_memory/cli/commands/project.py b/src/basic_memory/cli/commands/project.py index 3ef02c47..7f754283 100644 --- a/src/basic_memory/cli/commands/project.py +++ b/src/basic_memory/cli/commands/project.py @@ -157,29 +157,33 @@ def move_project( """Move a project to a new location.""" # Resolve to absolute path resolved_path = os.path.abspath(os.path.expanduser(new_path)) - + try: data = {"path": resolved_path} project_name = generate_permalink(name) - + current_project = session.get_current_project() - response = asyncio.run(call_patch(client, f"/{current_project}/project/{project_name}", json=data)) + response = asyncio.run( + call_patch(client, f"/{current_project}/project/{project_name}", json=data) + ) result = ProjectStatusResponse.model_validate(response.json()) - + console.print(f"[green]{result.message}[/green]") - + # Show important file movement reminder console.print() # Empty line for spacing - console.print(Panel( - "[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n" - "[yellow]You must manually move your project files from the old location to:[/yellow]\n" - f"[cyan]{resolved_path}[/cyan]\n\n" - "[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]", - title="⚠️ Manual File Movement Required", - border_style="yellow", - expand=False - )) - + console.print( + Panel( + "[bold red]IMPORTANT:[/bold red] Project configuration updated successfully.\n\n" + "[yellow]You must manually move your project files from the old location to:[/yellow]\n" + f"[cyan]{resolved_path}[/cyan]\n\n" + "[dim]Basic Memory has only updated the configuration - your files remain in their original location.[/dim]", + title="⚠️ Manual File Movement Required", + border_style="yellow", + expand=False, + ) + ) + except Exception as e: console.print(f"[red]Error moving project: {str(e)}[/red]") raise typer.Exit(1) diff --git a/src/basic_memory/importers/chatgpt_importer.py b/src/basic_memory/importers/chatgpt_importer.py index 0a00335a..4597aecc 100644 --- a/src/basic_memory/importers/chatgpt_importer.py +++ b/src/basic_memory/importers/chatgpt_importer.py @@ -209,24 +209,24 @@ class ChatGPTImporter(Importer[ChatImportResult]): # Use iterative approach with stack to avoid recursion depth issues stack = [root_id] - + while stack: node_id = stack.pop() if not node_id: continue - + node = mapping.get(node_id) if not node: continue - + # Process current node if it has a message and hasn't been seen if node["id"] not in seen and node.get("message"): seen.add(node["id"]) messages.append(node["message"]) - + # Add children to stack in reverse order to maintain conversation flow children = node.get("children", []) for child_id in reversed(children): stack.append(child_id) - + return messages diff --git a/src/basic_memory/repository/project_repository.py b/src/basic_memory/repository/project_repository.py index 630510f5..5e05742c 100644 --- a/src/basic_memory/repository/project_repository.py +++ b/src/basic_memory/repository/project_repository.py @@ -86,11 +86,11 @@ class ProjectRepository(Repository[Project]): async def update_path(self, project_id: int, new_path: str) -> Optional[Project]: """Update project path. - + Args: project_id: ID of the project to update new_path: New filesystem path for the project - + Returns: The updated project if found, None otherwise """ diff --git a/src/basic_memory/services/entity_service.py b/src/basic_memory/services/entity_service.py index 9adcad00..c33a8abd 100644 --- a/src/basic_memory/services/entity_service.py +++ b/src/basic_memory/services/entity_service.py @@ -47,35 +47,35 @@ class EntityService(BaseService[EntityModel]): async def detect_file_path_conflicts(self, file_path: str) -> List[Entity]: """Detect potential file path conflicts for a given file path. - + This checks for entities with similar file paths that might cause conflicts: - Case sensitivity differences (Finance/file.md vs finance/file.md) - Character encoding differences - Hyphen vs space differences - Unicode normalization differences - + Args: file_path: The file path to check for conflicts - + Returns: List of entities that might conflict with the given file path """ from basic_memory.utils import detect_potential_file_conflicts - + conflicts = [] - + # Get all existing file paths all_entities = await self.repository.find_all() existing_paths = [entity.file_path for entity in all_entities] - + # Use the enhanced conflict detection utility conflicting_paths = detect_potential_file_conflicts(file_path, existing_paths) - + # Find the entities corresponding to conflicting paths for entity in all_entities: if entity.file_path in conflicting_paths: conflicts.append(entity) - + return conflicts async def resolve_permalink( @@ -88,11 +88,11 @@ class EntityService(BaseService[EntityModel]): 2. If markdown has permalink but it's used by another file -> make unique 3. For existing files, keep current permalink from db 4. Generate new unique permalink from file path - + Enhanced to detect and handle character-related conflicts. """ file_path_str = str(file_path) - + # Check for potential file path conflicts before resolving permalink conflicts = await self.detect_file_path_conflicts(file_path_str) if conflicts: @@ -100,7 +100,7 @@ class EntityService(BaseService[EntityModel]): f"Detected potential file path conflicts for '{file_path_str}': " f"{[entity.file_path for entity in conflicts]}" ) - + # If markdown has explicit permalink, try to validate it if markdown and markdown.frontmatter.permalink: desired_permalink = markdown.frontmatter.permalink diff --git a/src/basic_memory/services/project_service.py b/src/basic_memory/services/project_service.py index 48518df1..3ebe1ea2 100644 --- a/src/basic_memory/services/project_service.py +++ b/src/basic_memory/services/project_service.py @@ -311,33 +311,33 @@ class ProjectService: async def move_project(self, name: str, new_path: str) -> None: """Move a project to a new location. - + Args: name: The name of the project to move new_path: The new absolute path for the project - + Raises: ValueError: If the project doesn't exist or repository isn't initialized """ if not self.repository: raise ValueError("Repository is required for move_project") - + # Resolve to absolute path resolved_path = os.path.abspath(os.path.expanduser(new_path)) - + # Validate project exists in config if name not in self.config_manager.projects: raise ValueError(f"Project '{name}' not found in configuration") - + # Create the new directory if it doesn't exist Path(resolved_path).mkdir(parents=True, exist_ok=True) - + # Update in configuration config = self.config_manager.load_config() old_path = config.projects[name] config.projects[name] = resolved_path self.config_manager.save_config(config) - + # Update in database project = await self.repository.get_by_name(name) if project: diff --git a/src/basic_memory/sync/sync_service.py b/src/basic_memory/sync/sync_service.py index d1619cde..a6568671 100644 --- a/src/basic_memory/sync/sync_service.py +++ b/src/basic_memory/sync/sync_service.py @@ -462,7 +462,7 @@ class SyncService: f"entity_id={entity.id} trying to move from '{old_path}' to '{new_path}', " f"but entity_id={existing_at_destination.id} already occupies '{new_path}'" ) - + # Check if this is a file swap (the destination entity is being moved to our old path) # This would indicate a simultaneous move operation old_path_after_swap = await self.entity_repository.get_by_file_path(old_path) diff --git a/src/basic_memory/utils.py b/src/basic_memory/utils.py index 2c5c989b..8459c0d8 100644 --- a/src/basic_memory/utils.py +++ b/src/basic_memory/utils.py @@ -183,74 +183,74 @@ def setup_logging( def normalize_file_path_for_comparison(file_path: str) -> str: """Normalize a file path for conflict detection. - + This function normalizes file paths to help detect potential conflicts: - Converts to lowercase for case-insensitive comparison - Normalizes Unicode characters - Handles path separators consistently - + Args: file_path: The file path to normalize - + Returns: Normalized file path for comparison purposes """ import unicodedata - + # Convert to lowercase for case-insensitive comparison normalized = file_path.lower() - + # Normalize Unicode characters (NFD normalization) - normalized = unicodedata.normalize('NFD', normalized) - + normalized = unicodedata.normalize("NFD", normalized) + # Replace path separators with forward slashes - normalized = normalized.replace('\\', '/') - + normalized = normalized.replace("\\", "/") + # Remove multiple slashes - normalized = re.sub(r'/+', '/', normalized) - + normalized = re.sub(r"/+", "/", normalized) + return normalized def detect_potential_file_conflicts(file_path: str, existing_paths: List[str]) -> List[str]: """Detect potential conflicts between a file path and existing paths. - + This function checks for various types of conflicts: - Case sensitivity differences - Unicode normalization differences - Path separator differences - Permalink generation conflicts - + Args: file_path: The file path to check existing_paths: List of existing file paths to check against - + Returns: List of existing paths that might conflict with the given file path """ conflicts = [] - + # Normalize the input file path normalized_input = normalize_file_path_for_comparison(file_path) input_permalink = generate_permalink(file_path) - + for existing_path in existing_paths: # Skip identical paths if existing_path == file_path: continue - + # Check for case-insensitive path conflicts normalized_existing = normalize_file_path_for_comparison(existing_path) if normalized_input == normalized_existing: conflicts.append(existing_path) continue - + # Check for permalink conflicts existing_permalink = generate_permalink(existing_path) if input_permalink == existing_permalink: conflicts.append(existing_path) continue - + return conflicts diff --git a/tests/api/test_project_router.py b/tests/api/test_project_router.py index 9966e1fb..8e68fd99 100644 --- a/tests/api/test_project_router.py +++ b/tests/api/test_project_router.py @@ -162,51 +162,52 @@ async def test_set_default_project_endpoint(test_config, client, project_service @pytest.mark.asyncio -async def test_update_project_path_endpoint(test_config, client, project_service, project_url, tmp_path): +async def test_update_project_path_endpoint( + test_config, client, project_service, project_url, tmp_path +): """Test the update project endpoint for changing project path.""" # Create a test project to update test_project_name = "test-update-project" old_path = str(tmp_path / "old-location") new_path = str(tmp_path / "new-location") - + await project_service.add_project(test_project_name, old_path) - + try: # Verify initial state project = await project_service.get_project(test_project_name) assert project is not None assert project.path == old_path - + # Update the project path response = await client.patch( - f"{project_url}/project/{test_project_name}", - json={"path": new_path} + f"{project_url}/project/{test_project_name}", json={"path": new_path} ) - + # Verify response assert response.status_code == 200 data = response.json() - + # Check response structure assert "message" in data assert "status" in data assert data["status"] == "success" assert "old_project" in data assert "new_project" in data - + # Check old project data assert data["old_project"]["name"] == test_project_name assert data["old_project"]["path"] == old_path - + # Check new project data assert data["new_project"]["name"] == test_project_name assert data["new_project"]["path"] == new_path - + # Verify project was actually updated in database updated_project = await project_service.get_project(test_project_name) assert updated_project is not None assert updated_project.path == new_path - + finally: # Clean up try: @@ -221,26 +222,25 @@ async def test_update_project_is_active_endpoint(test_config, client, project_se # Create a test project to update test_project_name = "test-update-active-project" test_path = "/tmp/test-update-active" - + await project_service.add_project(test_project_name, test_path) - + try: # Update the project is_active status response = await client.patch( - f"{project_url}/project/{test_project_name}", - json={"is_active": False} + f"{project_url}/project/{test_project_name}", json={"is_active": False} ) - + # Verify response assert response.status_code == 200 data = response.json() - + # Check response structure assert "message" in data assert "status" in data assert data["status"] == "success" assert f"Project '{test_project_name}' updated successfully" == data["message"] - + finally: # Clean up try: @@ -250,34 +250,36 @@ async def test_update_project_is_active_endpoint(test_config, client, project_se @pytest.mark.asyncio -async def test_update_project_both_params_endpoint(test_config, client, project_service, project_url, tmp_path): +async def test_update_project_both_params_endpoint( + test_config, client, project_service, project_url, tmp_path +): """Test the update project endpoint with both path and is_active parameters.""" # Create a test project to update test_project_name = "test-update-both-project" old_path = str(tmp_path / "old-location") new_path = str(tmp_path / "new-location") - + await project_service.add_project(test_project_name, old_path) - + try: # Update both path and is_active (path should take precedence) response = await client.patch( f"{project_url}/project/{test_project_name}", - json={"path": new_path, "is_active": False} + json={"path": new_path, "is_active": False}, ) - + # Verify response assert response.status_code == 200 data = response.json() - + # Check that path update was performed (takes precedence) assert data["new_project"]["path"] == new_path - + # Verify project was actually updated in database updated_project = await project_service.get_project(test_project_name) assert updated_project is not None assert updated_project.path == new_path - + finally: # Clean up try: @@ -291,10 +293,9 @@ async def test_update_project_nonexistent_endpoint(client, project_url): """Test the update project endpoint with a nonexistent project.""" # Try to update a project that doesn't exist response = await client.patch( - f"{project_url}/project/nonexistent-project", - json={"path": "/tmp/new-path"} + f"{project_url}/project/nonexistent-project", json={"path": "/tmp/new-path"} ) - + # Should return 400 error assert response.status_code == 400 data = response.json() @@ -303,27 +304,28 @@ async def test_update_project_nonexistent_endpoint(client, project_url): @pytest.mark.asyncio -async def test_update_project_relative_path_error_endpoint(test_config, client, project_service, project_url): +async def test_update_project_relative_path_error_endpoint( + test_config, client, project_service, project_url +): """Test the update project endpoint with relative path (should fail).""" # Create a test project to update test_project_name = "test-update-relative-project" test_path = "/tmp/test-update-relative" - + await project_service.add_project(test_project_name, test_path) - + try: # Try to update with relative path response = await client.patch( - f"{project_url}/project/{test_project_name}", - json={"path": "./relative-path"} + f"{project_url}/project/{test_project_name}", json={"path": "./relative-path"} ) - + # Should return 400 error assert response.status_code == 400 data = response.json() assert "detail" in data assert "Path must be absolute" in data["detail"] - + finally: # Clean up try: @@ -338,25 +340,22 @@ async def test_update_project_no_params_endpoint(test_config, client, project_se # Create a test project to update test_project_name = "test-update-no-params-project" test_path = "/tmp/test-update-no-params" - + await project_service.add_project(test_project_name, test_path) proj_info = await project_service.get_project(test_project_name) assert proj_info.name == test_project_name assert proj_info.path == test_path - + try: # Try to update with no parameters - response = await client.patch( - f"{project_url}/project/{test_project_name}", - json={} - ) - + response = await client.patch(f"{project_url}/project/{test_project_name}", json={}) + # Should return 200 (no-op) assert response.status_code == 200 proj_info = await project_service.get_project(test_project_name) assert proj_info.name == test_project_name assert proj_info.path == test_path - + finally: # Clean up try: @@ -366,26 +365,27 @@ async def test_update_project_no_params_endpoint(test_config, client, project_se @pytest.mark.asyncio -async def test_update_project_empty_path_endpoint(test_config, client, project_service, project_url): +async def test_update_project_empty_path_endpoint( + test_config, client, project_service, project_url +): """Test the update project endpoint with empty path parameter.""" # Create a test project to update test_project_name = "test-update-empty-path-project" test_path = "/tmp/test-update-empty-path" - + await project_service.add_project(test_project_name, test_path) - + try: # Try to update with empty/null path - should be treated as no path update response = await client.patch( - f"{project_url}/project/{test_project_name}", - json={"path": None, "is_active": True} + f"{project_url}/project/{test_project_name}", json={"path": None, "is_active": True} ) - + # Should succeed and perform is_active update assert response.status_code == 200 data = response.json() assert data["status"] == "success" - + finally: # Clean up try: diff --git a/tests/repository/test_project_repository.py b/tests/repository/test_project_repository.py index b2fd4385..62e3d1c3 100644 --- a/tests/repository/test_project_repository.py +++ b/tests/repository/test_project_repository.py @@ -273,16 +273,16 @@ async def test_delete_nonexistent_project(project_repository: ProjectRepository) async def test_update_path(project_repository: ProjectRepository, sample_project: Project): """Test updating a project's path.""" new_path = "/new/project/path" - + # Update the project path updated_project = await project_repository.update_path(sample_project.id, new_path) - + # Verify returned object assert updated_project is not None assert updated_project.id == sample_project.id assert updated_project.path == new_path assert updated_project.name == sample_project.name # Other fields unchanged - + # Verify in database found = await project_repository.find_by_id(sample_project.id) assert found is not None diff --git a/tests/services/test_project_service.py b/tests/services/test_project_service.py index 49076005..faf599f1 100644 --- a/tests/services/test_project_service.py +++ b/tests/services/test_project_service.py @@ -541,36 +541,36 @@ async def test_move_project(project_service: ProjectService, tmp_path): test_project_name = f"test-move-project-{os.urandom(4).hex()}" old_path = str(tmp_path / "old-location") new_path = str(tmp_path / "new-location") - + # Create old directory os.makedirs(old_path, exist_ok=True) - + try: # Add project with initial path await project_service.add_project(test_project_name, old_path) - + # Verify initial state assert test_project_name in project_service.projects assert project_service.projects[test_project_name] == old_path - + project = await project_service.repository.get_by_name(test_project_name) assert project is not None assert project.path == old_path - + # Move project to new location await project_service.move_project(test_project_name, new_path) - + # Verify config was updated assert project_service.projects[test_project_name] == new_path - + # Verify database was updated updated_project = await project_service.repository.get_by_name(test_project_name) assert updated_project is not None assert updated_project.path == new_path - + # Verify new directory was created assert os.path.exists(new_path) - + finally: # Clean up if test_project_name in project_service.projects: @@ -581,7 +581,7 @@ async def test_move_project(project_service: ProjectService, tmp_path): async def test_move_project_nonexistent(project_service: ProjectService, tmp_path): """Test moving a project that doesn't exist.""" new_path = str(tmp_path / "new-location") - + with pytest.raises(ValueError, match="not found in configuration"): await project_service.move_project("nonexistent-project", new_path) @@ -592,28 +592,28 @@ async def test_move_project_db_mismatch(project_service: ProjectService, tmp_pat test_project_name = f"test-move-mismatch-{os.urandom(4).hex()}" old_path = str(tmp_path / "old-location") new_path = str(tmp_path / "new-location") - + # Create directories os.makedirs(old_path, exist_ok=True) - + config_manager = project_service.config_manager - + try: # Add project to config only (not to database) config_manager.add_project(test_project_name, old_path) - + # Verify it's in config but not in database assert test_project_name in project_service.projects db_project = await project_service.repository.get_by_name(test_project_name) assert db_project is None - + # Try to move project - should fail and restore config with pytest.raises(ValueError, match="not found in database"): await project_service.move_project(test_project_name, new_path) - + # Verify config was restored to original path assert project_service.projects[test_project_name] == old_path - + finally: # Clean up if test_project_name in project_service.projects: @@ -625,28 +625,28 @@ async def test_move_project_expands_path(project_service: ProjectService, tmp_pa """Test that move_project expands ~ and relative paths.""" test_project_name = f"test-move-expand-{os.urandom(4).hex()}" old_path = str(tmp_path / "old-location") - + # Create old directory os.makedirs(old_path, exist_ok=True) - + try: # Add project with initial path await project_service.add_project(test_project_name, old_path) - + # Use a relative path for the move relative_new_path = "./new-location" expected_absolute_path = os.path.abspath(relative_new_path) - + # Move project using relative path await project_service.move_project(test_project_name, relative_new_path) - + # Verify the path was expanded to absolute assert project_service.projects[test_project_name] == expected_absolute_path - + updated_project = await project_service.repository.get_by_name(test_project_name) assert updated_project is not None assert updated_project.path == expected_absolute_path - + finally: # Clean up if test_project_name in project_service.projects: @@ -713,5 +713,3 @@ async def test_synchronize_projects_handles_case_sensitivity_bug( db_project = await project_service.repository.get_by_name(name) if db_project: await project_service.repository.delete(db_project.id) - - diff --git a/tests/sync/test_character_conflicts.py b/tests/sync/test_character_conflicts.py index e18b2357..5b218247 100644 --- a/tests/sync/test_character_conflicts.py +++ b/tests/sync/test_character_conflicts.py @@ -1,6 +1,5 @@ """Test character-related sync conflicts and permalink generation.""" -import asyncio from pathlib import Path from textwrap import dedent @@ -8,14 +7,12 @@ import pytest from sqlalchemy.exc import IntegrityError from basic_memory.config import ProjectConfig -from basic_memory.models import Entity from basic_memory.repository import EntityRepository -from basic_memory.services import EntityService from basic_memory.sync.sync_service import SyncService from basic_memory.utils import ( - generate_permalink, + generate_permalink, normalize_file_path_for_comparison, - detect_potential_file_conflicts + detect_potential_file_conflicts, ) @@ -31,29 +28,37 @@ class TestUtilityFunctions: 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" - + 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" - + 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" + 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", + "finance/Investment.md", "docs/my-feature.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 @@ -68,10 +73,10 @@ class TestPermalinkGeneration: # 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" @@ -79,7 +84,7 @@ class TestPermalinkGeneration: """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" @@ -93,11 +98,14 @@ class TestPermalinkGeneration: 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" - + 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" @@ -105,7 +113,7 @@ class TestPermalinkGeneration: """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" @@ -122,7 +130,7 @@ class TestSyncConflictHandling: ): """Test that file path conflicts are detected during move operations.""" project_dir = project_config.home - + # Create two files content1 = dedent(""" --- @@ -131,7 +139,7 @@ class TestSyncConflictHandling: # Document One This is the first document. """) - + content2 = dedent(""" --- type: knowledge @@ -139,44 +147,44 @@ class TestSyncConflictHandling: # 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, + 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(""" --- @@ -185,7 +193,7 @@ class TestSyncConflictHandling: # Basic Memory Bug This file has spaces in the name. """) - + # Create file with hyphens (already has hyphens in filename) content2 = dedent(""" --- @@ -194,17 +202,17 @@ class TestSyncConflictHandling: # 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" @@ -212,16 +220,16 @@ class TestSyncConflictHandling: async def test_case_sensitivity_conflict( self, sync_service: SyncService, - project_config: ProjectConfig, + project_config: ProjectConfig, entity_repository: EntityRepository, ): """Test conflict handling when case differences cause issues.""" 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 @@ -229,7 +237,7 @@ class TestSyncConflictHandling: # Investment Guide Upper case directory. """) - + content2 = dedent(""" --- type: knowledge @@ -237,17 +245,17 @@ class TestSyncConflictHandling: # 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() assert len(entities) >= 2 # Allow for potential other test files - + # Check that file paths are preserved correctly file_paths = [entity.file_path for entity in entities] assert "Finance/investment.md" in file_paths @@ -261,44 +269,44 @@ class TestSyncConflictHandling: ): """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_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 +@pytest.mark.asyncio class TestEnhancedErrorMessages: """Test that error messages provide helpful guidance for character conflicts.""" @@ -316,6 +324,6 @@ class TestEnhancedErrorMessages: sync_service: SyncService, project_config: ProjectConfig, ): - """Test that case sensitivity conflicts generate helpful error messages.""" + """Test that case sensitivity conflicts generate helpful error messages.""" # This test will be implemented after we enhance the error handling - pass \ No newline at end of file + pass