@@ -756,7 +756,15 @@ async def test_synchronize_projects_handles_case_sensitivity_bug(project_service
756756async def test_add_project_with_project_root_sanitizes_paths (
757757 project_service : ProjectService , config_manager : ConfigManager , monkeypatch
758758):
759- """Test that BASIC_MEMORY_PROJECT_ROOT sanitizes and validates project paths."""
759+ """Test that BASIC_MEMORY_PROJECT_ROOT uses sanitized project name, ignoring user path.
760+
761+ When project_root is set (cloud mode), the system should:
762+ 1. Ignore the user's provided path completely
763+ 2. Use the sanitized project name as the directory name
764+ 3. Create a flat structure: /app/data/test-bisync instead of /app/data/documents/test bisync
765+
766+ This prevents the bisync auto-discovery bug where nested paths caused duplicate project creation.
767+ """
760768 with tempfile .TemporaryDirectory () as temp_dir :
761769 # Set up project root environment
762770 project_root_path = Path (temp_dir ) / "app" / "data"
@@ -770,65 +778,48 @@ async def test_add_project_with_project_root_sanitizes_paths(
770778 config_module ._CONFIG_CACHE = None
771779
772780 test_cases = [
773- # (input_path, expected_result_path, should_succeed)
774- ("test" , str (project_root_path / "test" ), True ), # Simple relative path
775- (
776- "~/Documents/test" ,
777- str (project_root_path / "Documents" / "test" ),
778- True ,
779- ), # Home directory
780- (
781- "/tmp/test" ,
782- str (project_root_path / "tmp" / "test" ),
783- True ,
784- ), # Absolute path (sanitized to relative)
785- (
786- "../../../etc/passwd" ,
787- str (project_root_path ),
788- True ,
789- ), # Path traversal (all ../ removed, results in project_root)
790- (
791- "folder/subfolder" ,
792- str (project_root_path / "folder" / "subfolder" ),
793- True ,
794- ), # Nested path
795- (
796- "~/folder/../test" ,
797- str (project_root_path / "test" ),
798- True ,
799- ), # Mixed patterns (sanitized to just 'test')
781+ # (project_name, user_path, expected_sanitized_name)
782+ # User path is IGNORED - only project name matters
783+ ("test" , "anything/path" , "test" ),
784+ ("Test BiSync" , "~/Documents/Test BiSync" , "test-bi-sync" ), # BiSync -> bi-sync (dash preserved)
785+ ("My Project" , "/tmp/whatever" , "my-project" ),
786+ ("UPPERCASE" , "~" , "uppercase" ),
787+ ("With Spaces" , "~/Documents/With Spaces" , "with-spaces" ),
800788 ]
801789
802- for i , (input_path , expected_path , should_succeed ) in enumerate (test_cases ):
803- test_project_name = f"project-root-test-{ i } "
790+ for i , (project_name , user_path , expected_sanitized ) in enumerate (test_cases ):
791+ test_project_name = f"{ project_name } -{ i } " # Make unique
792+ expected_final_segment = f"{ expected_sanitized } -{ i } "
804793
805794 try :
806- # Add the project
807- await project_service .add_project (test_project_name , input_path )
795+ # Add the project - user_path should be ignored
796+ await project_service .add_project (test_project_name , user_path )
797+
798+ # Verify the path uses sanitized project name, not user path
799+ assert test_project_name in project_service .projects
800+ actual_path = project_service .projects [test_project_name ]
801+
802+ # The path should be under project_root (resolve both to handle macOS /private/var)
803+ assert (
804+ Path (actual_path )
805+ .resolve ()
806+ .is_relative_to (Path (project_root_path ).resolve ())
807+ ), (
808+ f"Path { actual_path } should be under { project_root_path } "
809+ )
808810
809- if should_succeed :
810- # Verify the path was sanitized correctly
811- assert test_project_name in project_service .projects
812- actual_path = project_service .projects [test_project_name ]
813-
814- # The path should be under project_root (resolve both to handle macOS /private/var)
815- assert (
816- Path (actual_path )
817- .resolve ()
818- .is_relative_to (Path (project_root_path ).resolve ())
819- ), (
820- f"Path { actual_path } should be under { project_root_path } for input { input_path } "
821- )
822-
823- # Clean up
824- await project_service .remove_project (test_project_name )
825- else :
826- pytest .fail (f"Expected ValueError for input path: { input_path } " )
811+ # Verify the final path segment is the sanitized project name
812+ path_parts = Path (actual_path ).parts
813+ final_segment = path_parts [- 1 ]
814+ assert final_segment == expected_final_segment , (
815+ f"Expected path segment '{ expected_final_segment } ', got '{ final_segment } '"
816+ )
817+
818+ # Clean up
819+ await project_service .remove_project (test_project_name )
827820
828821 except ValueError as e :
829- if should_succeed :
830- pytest .fail (f"Unexpected ValueError for input path { input_path } : { e } " )
831- # Expected failure - continue to next test case
822+ pytest .fail (f"Unexpected ValueError for project { test_project_name } : { e } " )
832823
833824
834825@pytest .mark .skipif (os .name == "nt" , reason = "Project root constraints only tested on POSIX systems" )
@@ -919,12 +910,17 @@ async def test_add_project_without_project_root_allows_arbitrary_paths(
919910 await project_service .remove_project (test_project_name )
920911
921912
913+ @pytest .mark .skip (reason = "Obsolete: project_root mode now uses sanitized project name, not user path. See test_add_project_with_project_root_sanitizes_paths instead." )
922914@pytest .mark .skipif (os .name == "nt" , reason = "Project root constraints only tested on POSIX systems" )
923915@pytest .mark .asyncio
924916async def test_add_project_with_project_root_normalizes_case (
925917 project_service : ProjectService , config_manager : ConfigManager , monkeypatch
926918):
927- """Test that BASIC_MEMORY_PROJECT_ROOT normalizes paths to lowercase."""
919+ """Test that BASIC_MEMORY_PROJECT_ROOT normalizes paths to lowercase.
920+
921+ NOTE: This test is obsolete. After fixing the bisync duplicate project bug,
922+ project_root mode now ignores the user's path and uses the sanitized project name instead.
923+ """
928924 with tempfile .TemporaryDirectory () as temp_dir :
929925 # Set up project root environment
930926 project_root_path = Path (temp_dir ) / "app" / "data"
@@ -966,12 +962,17 @@ async def test_add_project_with_project_root_normalizes_case(
966962 pytest .fail (f"Unexpected ValueError for input path { input_path } : { e } " )
967963
968964
965+ @pytest .mark .skip (reason = "Obsolete: project_root mode now uses sanitized project name, not user path." )
969966@pytest .mark .skipif (os .name == "nt" , reason = "Project root constraints only tested on POSIX systems" )
970967@pytest .mark .asyncio
971968async def test_add_project_with_project_root_detects_case_collisions (
972969 project_service : ProjectService , config_manager : ConfigManager , monkeypatch
973970):
974- """Test that BASIC_MEMORY_PROJECT_ROOT detects case-insensitive path collisions."""
971+ """Test that BASIC_MEMORY_PROJECT_ROOT detects case-insensitive path collisions.
972+
973+ NOTE: This test is obsolete. After fixing the bisync duplicate project bug,
974+ project_root mode now ignores the user's path and uses the sanitized project name instead.
975+ """
975976 with tempfile .TemporaryDirectory () as temp_dir :
976977 # Set up project root environment
977978 project_root_path = Path (temp_dir ) / "app" / "data"
@@ -1162,21 +1163,30 @@ async def test_add_project_nested_validation_with_project_root(
11621163 child_project_name = f"cloud-child-{ os .urandom (4 ).hex ()} "
11631164
11641165 try :
1165- # Add parent project (normalized to lowercase)
1166+ # Add parent project - user path is ignored, uses sanitized project name
11661167 await project_service .add_project (parent_project_name , "parent-folder" )
11671168
1168- # Verify it was created
1169+ # Verify it was created using sanitized project name, not user path
11691170 assert parent_project_name in project_service .projects
11701171 parent_actual_path = project_service .projects [parent_project_name ]
1171- # Resolve both paths to handle macOS /private/var vs /var differences
1172- assert (
1173- Path (parent_actual_path ).resolve ()
1174- == (project_root_path / "parent-folder" ).resolve ()
1175- )
1176-
1177- # Try to add a child project nested under parent (should fail)
1178- with pytest .raises (ValueError , match = "nested within existing project" ):
1179- await project_service .add_project (child_project_name , "parent-folder/child-folder" )
1172+ # Path should use sanitized project name (cloud-parent-xxx -> cloud-parent-xxx)
1173+ # NOT the user-provided path "parent-folder"
1174+ assert parent_project_name .lower () in parent_actual_path .lower ()
1175+ # Resolve both to handle macOS /private/var vs /var
1176+ assert Path (parent_actual_path ).resolve ().is_relative_to (Path (project_root_path ).resolve ())
1177+
1178+ # Nested projects should still be prevented, even with user path ignored
1179+ # Since paths use project names, this won't actually be nested
1180+ # But we can test that two projects can coexist
1181+ await project_service .add_project (child_project_name , "parent-folder/child-folder" )
1182+
1183+ # Both should exist with their own paths
1184+ assert child_project_name in project_service .projects
1185+ child_actual_path = project_service .projects [child_project_name ]
1186+ assert child_project_name .lower () in child_actual_path .lower ()
1187+
1188+ # Clean up child
1189+ await project_service .remove_project (child_project_name )
11801190
11811191 finally :
11821192 # Clean up
0 commit comments