@@ -112,6 +112,34 @@ def test_invalid_definitive_mapping_with_no_fallback_returns_none(self) -> None:
112112 self .assertIsNone (assigned )
113113
114114
115+ class TestDetermineProjectStageOrdering (unittest .TestCase ):
116+ """Non-definitive stages are tried in fixed order; earlier stage wins on conflict."""
117+
118+ def test_newly_created_files_wins_over_conflicting_code_block_data (self ) -> None :
119+ with tempfile .TemporaryDirectory () as tmp :
120+ ws_a_root = os .path .join (tmp , "project-a" )
121+ ws_b_root = os .path .join (tmp , "project-b" )
122+ os .makedirs (ws_a_root , exist_ok = True )
123+ os .makedirs (ws_b_root , exist_ok = True )
124+ entries = [
125+ _write_workspace_json (tmp , "ws-a" , ws_a_root ),
126+ _write_workspace_json (tmp , "ws-b" , ws_b_root ),
127+ ]
128+ file_a = os .path .join (ws_a_root , "src" , "a.py" )
129+ file_b = os .path .join (ws_b_root , "src" , "b.py" )
130+ os .makedirs (os .path .dirname (file_a ), exist_ok = True )
131+ os .makedirs (os .path .dirname (file_b ), exist_ok = True )
132+
133+ assigned = _resolve (
134+ {
135+ "newlyCreatedFiles" : [{"uri" : {"path" : file_a }}],
136+ "codeBlockData" : {f"file://{ file_b } " : {"language" : "python" }},
137+ },
138+ workspace_entries = entries ,
139+ )
140+ self .assertEqual (assigned , "ws-a" )
141+
142+
115143class TestDetermineProjectLayoutsStage (unittest .TestCase ):
116144 def test_project_layouts_resolves_via_workspace_path_to_id (self ) -> None :
117145 root = normalize_file_path ("/work/repos/myapp" )
@@ -133,7 +161,8 @@ def test_project_layouts_resolves_via_project_name_fallback(self) -> None:
133161 )
134162 self .assertEqual (assigned , "ws-from-name" )
135163
136- def test_missing_project_layouts_key_falls_through (self ) -> None :
164+ def test_newly_created_files_resolves_without_project_layouts_entry (self ) -> None :
165+ """No projectLayouts row: newlyCreatedFiles still resolves via workspace_entries."""
137166 with tempfile .TemporaryDirectory () as tmp :
138167 ws_root = os .path .join (tmp , "proj" )
139168 os .makedirs (ws_root , exist_ok = True )
@@ -148,6 +177,24 @@ def test_missing_project_layouts_key_falls_through(self) -> None:
148177 )
149178 self .assertEqual (assigned , "ws-entries" )
150179
180+ def test_unresolvable_project_layouts_falls_through_to_file_paths (self ) -> None :
181+ """projectLayouts roots that do not map still allow later file-path stages."""
182+ with tempfile .TemporaryDirectory () as tmp :
183+ ws_root = os .path .join (tmp , "fallback-proj" )
184+ os .makedirs (ws_root , exist_ok = True )
185+ entries = [_write_workspace_json (tmp , "ws-fallback" , ws_root )]
186+ inside = os .path .join (ws_root , "lib" , "mod.py" )
187+ os .makedirs (os .path .dirname (inside ), exist_ok = True )
188+ unmapped_root = normalize_file_path ("/no/such/workspace/root" )
189+
190+ assigned = _resolve (
191+ {"newlyCreatedFiles" : [{"uri" : {"path" : inside }}]},
192+ composer_id = "cmp-unmapped-layout" ,
193+ project_layouts_map = {"cmp-unmapped-layout" : [unmapped_root ]},
194+ workspace_entries = entries ,
195+ )
196+ self .assertEqual (assigned , "ws-fallback" )
197+
151198
152199class TestDetermineProjectFilePathStages (unittest .TestCase ):
153200 def test_newly_created_files_resolves_workspace (self ) -> None :
@@ -290,6 +337,23 @@ def test_longer_folder_name_wins_path_segment_tie(self) -> None:
290337 )
291338 self .assertEqual (assigned , "ws-long" )
292339
340+ def test_path_segment_matching_from_bubble_relevant_files (self ) -> None :
341+ """Path-segment last resort can use bubble file refs when composer has no file keys."""
342+ with tempfile .TemporaryDirectory () as tmp :
343+ ws_root = os .path .join (tmp , "storage" , "bubbleseg" )
344+ os .makedirs (ws_root , exist_ok = True )
345+ entries = [_write_workspace_json (tmp , "ws-bubble-seg" , ws_root )]
346+
347+ orphan = os .path .join (tmp , "external" , "bubbleseg" , "orphan.py" )
348+ os .makedirs (os .path .dirname (orphan ), exist_ok = True )
349+
350+ assigned = _resolve (
351+ {"fullConversationHeadersOnly" : [{"bubbleId" : "b-seg" }]},
352+ bubble_map = {"b-seg" : {"relevantFiles" : [orphan ]}},
353+ workspace_entries = entries ,
354+ )
355+ self .assertEqual (assigned , "ws-bubble-seg" )
356+
293357
294358class TestDetermineProjectTerminalNone (unittest .TestCase ):
295359 def test_all_stages_fail_returns_none (self ) -> None :
@@ -398,6 +462,29 @@ def test_valid_definitive_mapping_always_wins(
398462 )
399463 self .assertEqual (result , workspace_id )
400464
465+ @given (
466+ composer_id = st .text (min_size = 1 , max_size = 40 ),
467+ invalid_id = st .text (min_size = 1 , max_size = 40 ),
468+ )
469+ @settings (max_examples = 60 , deadline = None )
470+ def test_invalid_definitive_mapping_never_returned_without_fallback (
471+ self , composer_id : str , invalid_id : str
472+ ) -> None :
473+ """Ignored invalid mapping must not leak through when no heuristic stage matches."""
474+ result = determine_project_for_conversation (
475+ composer_data = dict (_EMPTY_COMPOSER ),
476+ composer_id = composer_id ,
477+ project_layouts_map = {},
478+ project_name_to_workspace_id = {},
479+ workspace_path_to_id = {},
480+ workspace_entries = [],
481+ bubble_map = {},
482+ composer_id_to_workspace_id = {composer_id : invalid_id },
483+ invalid_workspace_ids = {invalid_id },
484+ )
485+ self .assertNotEqual (result , invalid_id )
486+ self .assertIsNone (result )
487+
401488
402489if __name__ == "__main__" :
403490 unittest .main ()
0 commit comments