@@ -959,3 +959,81 @@ def test_search_action_names_with_duplicates(self, mock_search: MagicMock) -> No
959959 assert results [0 ].id == "breathehr_1.0.0_breathehr_list_employees_global"
960960 # Sorted by score descending
961961 assert results [0 ].similarity_score == 0.95
962+
963+
964+ class TestZeroMatchFallback :
965+ """Tests for fallback when semantic results don't match MCP tools."""
966+
967+ @patch .object (SemanticSearchClient , "search" )
968+ @patch ("stackone_ai.toolset._fetch_mcp_tools" )
969+ def test_auto_mode_falls_back_when_no_tools_match (
970+ self ,
971+ mock_fetch : MagicMock ,
972+ mock_search : MagicMock ,
973+ ) -> None :
974+ """Auto mode falls back to local search when semantic results don't match any MCP tools."""
975+ from stackone_ai import StackOneToolSet
976+ from stackone_ai .toolset import _McpToolDefinition
977+
978+ # Semantic returns results with IDs that won't match MCP tool names
979+ mock_search .return_value = SemanticSearchResponse (
980+ results = [
981+ SemanticSearchResult (
982+ id = "unknown_1.0.0_nonexistent_action_global" ,
983+ similarity_score = 0.95 ,
984+ ),
985+ ],
986+ total_count = 1 ,
987+ query = "manage employees" ,
988+ )
989+
990+ mock_fetch .return_value = [
991+ _McpToolDefinition (
992+ name = "bamboohr_create_employee" ,
993+ description = "Creates a new employee in BambooHR" ,
994+ input_schema = {"type" : "object" , "properties" : {}},
995+ ),
996+ ]
997+
998+ toolset = StackOneToolSet (api_key = "test-key" , search = {"method" : "auto" })
999+ tools = toolset .search_tools ("manage employees" , top_k = 5 )
1000+
1001+ # Should fall back to local search and return results (not empty)
1002+ assert len (tools ) > 0
1003+
1004+ @patch .object (SemanticSearchClient , "search" )
1005+ @patch ("stackone_ai.toolset._fetch_mcp_tools" )
1006+ def test_semantic_mode_does_not_fall_back (
1007+ self ,
1008+ mock_fetch : MagicMock ,
1009+ mock_search : MagicMock ,
1010+ ) -> None :
1011+ """Semantic mode returns empty results when no tools match, does not fall back."""
1012+ from stackone_ai import StackOneToolSet
1013+ from stackone_ai .toolset import _McpToolDefinition
1014+
1015+ # Semantic returns results with IDs that won't match MCP tool names
1016+ mock_search .return_value = SemanticSearchResponse (
1017+ results = [
1018+ SemanticSearchResult (
1019+ id = "unknown_1.0.0_nonexistent_action_global" ,
1020+ similarity_score = 0.95 ,
1021+ ),
1022+ ],
1023+ total_count = 1 ,
1024+ query = "manage employees" ,
1025+ )
1026+
1027+ mock_fetch .return_value = [
1028+ _McpToolDefinition (
1029+ name = "bamboohr_create_employee" ,
1030+ description = "Creates a new employee in BambooHR" ,
1031+ input_schema = {"type" : "object" , "properties" : {}},
1032+ ),
1033+ ]
1034+
1035+ toolset = StackOneToolSet (api_key = "test-key" , search = {"method" : "auto" })
1036+ tools = toolset .search_tools ("manage employees" , search = "semantic" , top_k = 5 )
1037+
1038+ # Semantic mode should return empty, not fall back
1039+ assert len (tools ) == 0
0 commit comments