@@ -867,3 +867,96 @@ async def test_add_project_without_project_root_allows_arbitrary_paths(
867867 # Clean up
868868 if test_project_name in project_service .projects :
869869 await project_service .remove_project (test_project_name )
870+
871+
872+ @pytest .mark .skipif (os .name == "nt" , reason = "Project root constraints only tested on POSIX systems" )
873+ @pytest .mark .asyncio
874+ async def test_add_project_with_project_root_normalizes_case (
875+ project_service : ProjectService , config_manager : ConfigManager , tmp_path , monkeypatch
876+ ):
877+ """Test that BASIC_MEMORY_PROJECT_ROOT normalizes paths to lowercase."""
878+ # Set up project root environment
879+ project_root_path = tmp_path / "app" / "data"
880+ project_root_path .mkdir (parents = True , exist_ok = True )
881+
882+ monkeypatch .setenv ("BASIC_MEMORY_PROJECT_ROOT" , str (project_root_path ))
883+
884+ # Invalidate config cache so it picks up the new env var
885+ from basic_memory import config as config_module
886+
887+ config_module ._CONFIG_CACHE = None
888+
889+ test_cases = [
890+ # (input_path, expected_normalized_path)
891+ ("Documents/my-project" , str (project_root_path / "documents" / "my-project" )),
892+ ("UPPERCASE/PATH" , str (project_root_path / "uppercase" / "path" )),
893+ ("MixedCase/Path" , str (project_root_path / "mixedcase" / "path" )),
894+ ("documents/Test-TWO" , str (project_root_path / "documents" / "test-two" )),
895+ ]
896+
897+ for i , (input_path , expected_path ) in enumerate (test_cases ):
898+ test_project_name = f"case-normalize-test-{ i } "
899+
900+ try :
901+ # Add the project
902+ await project_service .add_project (test_project_name , input_path )
903+
904+ # Verify the path was normalized to lowercase
905+ assert test_project_name in project_service .projects
906+ actual_path = project_service .projects [test_project_name ]
907+ assert actual_path == expected_path , (
908+ f"Expected path { expected_path } but got { actual_path } for input { input_path } "
909+ )
910+
911+ # Clean up
912+ await project_service .remove_project (test_project_name )
913+
914+ except ValueError as e :
915+ pytest .fail (f"Unexpected ValueError for input path { input_path } : { e } " )
916+
917+
918+ @pytest .mark .skipif (os .name == "nt" , reason = "Project root constraints only tested on POSIX systems" )
919+ @pytest .mark .asyncio
920+ async def test_add_project_with_project_root_detects_case_collisions (
921+ project_service : ProjectService , config_manager : ConfigManager , tmp_path , monkeypatch
922+ ):
923+ """Test that BASIC_MEMORY_PROJECT_ROOT detects case-insensitive path collisions."""
924+ # Set up project root environment
925+ project_root_path = tmp_path / "app" / "data"
926+ project_root_path .mkdir (parents = True , exist_ok = True )
927+
928+ monkeypatch .setenv ("BASIC_MEMORY_PROJECT_ROOT" , str (project_root_path ))
929+
930+ # Invalidate config cache so it picks up the new env var
931+ from basic_memory import config as config_module
932+
933+ config_module ._CONFIG_CACHE = None
934+
935+ # First, create a project with lowercase path
936+ first_project = "documents-project"
937+ await project_service .add_project (first_project , "documents/basic-memory" )
938+
939+ # Verify it was created with normalized lowercase path
940+ assert first_project in project_service .projects
941+ first_path = project_service .projects [first_project ]
942+ assert first_path == str (project_root_path / "documents" / "basic-memory" )
943+
944+ # Now try to create a project with the same path but different case
945+ # This should be normalized to the same lowercase path and not cause a collision
946+ # since both will be normalized to the same path
947+ second_project = "documents-project-2"
948+ try :
949+ # This should succeed because both get normalized to the same lowercase path
950+ await project_service .add_project (second_project , "documents/basic-memory" )
951+ # If we get here, both should have the exact same path
952+ second_path = project_service .projects [second_project ]
953+ assert second_path == first_path
954+
955+ # Clean up second project
956+ await project_service .remove_project (second_project )
957+ except ValueError :
958+ # This is expected if there's already a project with this exact path
959+ pass
960+
961+ # Clean up
962+ await project_service .remove_project (first_project )
0 commit comments