@@ -721,10 +721,59 @@ async def test_find_experiment_in_project_returns_matching_experiments(self, moc
721721 assert result [0 ]["name" ] == "Training Experiment"
722722 assert result [1 ]["name" ] == "Validation Experiment"
723723 # The pattern is matched server-side via task_name, not by hydrating each task.
724+ # It is built as a case-insensitive literal-substring regex.
724725 _ , kwargs = mock_task .query_tasks .call_args
725- assert kwargs ["task_name" ] == "experiment"
726+ assert kwargs ["task_name" ] == "(?i) experiment"
726727 mock_task .get_task .assert_not_called ()
727728
729+ @pytest .mark .asyncio
730+ @patch ("clearml_mcp.clearml_mcp.Task" )
731+ async def test_find_experiment_escapes_regex_special_characters (self , mock_task ):
732+ """find_experiment_in_project matches the pattern literally, not as a regex.
733+
734+ ClearML treats task_name as a regex, so metacharacters must be escaped:
735+ "a.b" should match only the literal "a.b" (not "axb"), and "exp[1]" must
736+ not error the query.
737+ """
738+ mock_task .query_tasks .side_effect = _make_query_tasks ([])
739+ mock_task .get_projects .return_value = []
740+
741+ result = await clearml_mcp .find_experiment_in_project .fn ("ML Project" , "exp[1].a" )
742+
743+ assert result == []
744+ _ , kwargs = mock_task .query_tasks .call_args
745+ # Special characters are escaped (literal match) and case-insensitive.
746+ assert kwargs ["task_name" ] == r"(?i)exp\[1\]\.a"
747+ mock_task .get_task .assert_not_called ()
748+
749+ @pytest .mark .asyncio
750+ @patch ("clearml_mcp.clearml_mcp.Task" )
751+ async def test_find_experiment_is_case_insensitive (self , mock_task ):
752+ """find_experiment_in_project preserves case-insensitive matching."""
753+ mock_task .query_tasks .side_effect = _make_query_tasks (
754+ [
755+ {
756+ "id" : "task_1" ,
757+ "name" : "Training EXPERIMENT" ,
758+ "status" : "completed" ,
759+ "type" : "training" ,
760+ "comment" : "" ,
761+ "created" : "2024-01-01T00:00:00Z" ,
762+ "project" : "proj_1" ,
763+ "tags" : [],
764+ }
765+ ]
766+ )
767+ mock_task .get_projects .return_value = [_fake_project ("proj_1" , "ML Project" )]
768+
769+ result = await clearml_mcp .find_experiment_in_project .fn ("ML Project" , "experiment" )
770+
771+ # The (?i) prefix makes the server match regardless of case.
772+ _ , kwargs = mock_task .query_tasks .call_args
773+ assert kwargs ["task_name" ].startswith ("(?i)" )
774+ assert len (result ) == 1
775+ assert result [0 ]["name" ] == "Training EXPERIMENT"
776+
728777 @pytest .mark .asyncio
729778 @patch ("clearml_mcp.clearml_mcp.Task" )
730779 async def test_find_experiment_returns_error_on_query_failure (self , mock_task ):
0 commit comments