@@ -72,6 +72,16 @@ def setUp(self):
7272 "request_timeout" : 30.0
7373 }
7474 }
75+ self .mistral_config_data = {
76+ "llm_provider" : "mistral" ,
77+ "mistral" : {
78+ "api_key" : "test_mistral_api_key" ,
79+ "default_model" : "open-mistral-7b" ,
80+ "temperature" : 0.5 ,
81+ "max_tokens" : 512 ,
82+ "request_timeout" : 30.0
83+ }
84+ }
7585 self .bedrock_config_data = {
7686 "llm_provider" : "bedrock" ,
7787 "bedrock" : {
@@ -273,6 +283,43 @@ def test_init_together_placeholder_api_key_becomes_empty(self):
273283 self .assertEqual (llm_interface .api_key , "" )
274284 self .assertTrue (any ("Using empty API key for Together.ai" in msg for msg in log_watcher .output ))
275285
286+ @patch .dict (os .environ , {}, clear = True )
287+ def test_init_mistral_api_key_from_env (self ):
288+ """Test Mistral API key loaded from environment variable."""
289+ os .environ ["MISTRAL_API_KEY" ] = "env_mistral_key"
290+ self ._create_llm_config_file (self .mistral_config_data )
291+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
292+ self .assertEqual (llm_interface .provider , "mistral" )
293+ self .assertEqual (llm_interface .api_key , "env_mistral_key" )
294+ self .assertEqual (llm_interface .model_name , self .mistral_config_data ["mistral" ]["default_model" ])
295+
296+ @patch .dict (os .environ , {}, clear = True )
297+ def test_init_mistral_api_key_from_yaml_if_not_in_env (self ):
298+ """Test Mistral API key loaded from YAML when not in environment."""
299+ self ._create_llm_config_file (self .mistral_config_data )
300+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
301+ self .assertEqual (llm_interface .provider , "mistral" )
302+ self .assertEqual (llm_interface .api_key , "test_mistral_api_key" )
303+ self .assertEqual (llm_interface .model_name , self .mistral_config_data ["mistral" ]["default_model" ])
304+
305+ @patch .dict (os .environ , {}, clear = True )
306+ def test_init_mistral_config_section_missing_and_env_var_unset_raises_error (self ):
307+ """Test ConfigError if Mistral provider section is missing and env var not set."""
308+ config_missing = {"llm_provider" : "mistral" }
309+ self ._create_llm_config_file (config_missing )
310+ with self .assertRaisesRegex (ConfigError , "Mistral configuration missing and MISTRAL_API_KEY environment variable not set." ):
311+ LLMInterface (config_loader = self .mock_config_loader )
312+
313+ @patch .dict (os .environ , {}, clear = True )
314+ def test_init_mistral_placeholder_api_key_becomes_empty (self ):
315+ """Test placeholder Mistral API key becomes empty when env var not set."""
316+ config_placeholder = {"llm_provider" : "mistral" , "mistral" : {"api_key" : "YOUR_MISTRAL_API_KEY_PLACEHOLDER" , "default_model" : "m" }}
317+ self ._create_llm_config_file (config_placeholder )
318+ with self .assertLogs ('llm_controller.llm_interface' , level = 'WARNING' ) as log_watcher :
319+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
320+ self .assertEqual (llm_interface .api_key , "" )
321+ self .assertTrue (any ("Using empty API key for Mistral" in msg for msg in log_watcher .output ))
322+
276323 # --- End API Key Handling Tests ---
277324
278325 # --- Gemini API Call Tests ---
@@ -519,6 +566,83 @@ async def test_get_llm_action_json_together_malformed_json(self, MockTogether):
519566 await llm_interface .get_llm_action_json ([{"role" : "user" , "content" : "tap" }])
520567 # --- End Together API Call Tests ---
521568
569+ # --- Mistral API Call Tests ---
570+ @patch ('llm_controller.llm_interface.httpx.AsyncClient' )
571+ async def test_get_llm_action_json_mistral_success (self , MockAsyncClient ):
572+ """Test successful Mistral API call and JSON response parsing."""
573+ self ._create_llm_config_file (self .mistral_config_data )
574+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
575+
576+ mock_api_resp_structure = {
577+ "choices" : [
578+ {"message" : {"content" : '{"action": "tap"}' }}
579+ ]
580+ }
581+
582+ mock_response = MagicMock (spec = httpx .Response )
583+ mock_response .status_code = 200
584+ mock_response .json .return_value = mock_api_resp_structure
585+
586+ mock_client_instance = MockAsyncClient .return_value .__aenter__ .return_value
587+ mock_client_instance .post = AsyncMock (return_value = mock_response )
588+
589+ result_json = await llm_interface .get_llm_action_json ([{"role" : "user" , "content" : "tap" }])
590+ self .assertEqual (result_json , {"action" : "tap" })
591+ mock_client_instance .post .assert_called_once ()
592+
593+ @patch ('llm_controller.llm_interface.httpx.AsyncClient' )
594+ async def test_get_llm_action_json_mistral_http_status_error (self , MockAsyncClient ):
595+ """Test Mistral API HTTPStatusError handling."""
596+ self ._create_llm_config_file (self .mistral_config_data )
597+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
598+
599+ mock_http_response = MagicMock (spec = httpx .Response )
600+ mock_http_response .status_code = 429
601+ mock_http_response .text = "Rate limited"
602+
603+ mock_client_instance = MockAsyncClient .return_value .__aenter__ .return_value
604+ mock_client_instance .post = AsyncMock (side_effect = httpx .HTTPStatusError (
605+ "Too Many Requests" , request = MagicMock (), response = mock_http_response
606+ ))
607+
608+ with self .assertRaisesRegex (LLMInterfaceError , "Mistral API request failed: 429 - Rate limited" ):
609+ await llm_interface .get_llm_action_json ([{"role" : "user" , "content" : "tap" }])
610+
611+ @patch ('llm_controller.llm_interface.httpx.AsyncClient' )
612+ async def test_get_llm_action_json_mistral_request_error (self , MockAsyncClient ):
613+ """Test Mistral API request error handling."""
614+ self ._create_llm_config_file (self .mistral_config_data )
615+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
616+
617+ mock_client_instance = MockAsyncClient .return_value .__aenter__ .return_value
618+ mock_client_instance .post = AsyncMock (side_effect = httpx .ConnectError ("conn fail" ))
619+
620+ with self .assertRaisesRegex (LLMInterfaceError , "Error during Mistral API call: conn fail" ):
621+ await llm_interface .get_llm_action_json ([{"role" : "user" , "content" : "tap" }])
622+
623+ @patch ('llm_controller.llm_interface.httpx.AsyncClient' )
624+ async def test_get_llm_action_json_mistral_malformed_json (self , MockAsyncClient ):
625+ """Test Mistral API returning malformed JSON."""
626+ self ._create_llm_config_file (self .mistral_config_data )
627+ llm_interface = LLMInterface (config_loader = self .mock_config_loader )
628+
629+ mock_api_resp_structure = {
630+ "choices" : [
631+ {"message" : {"content" : 'not json' }}
632+ ]
633+ }
634+
635+ mock_response = MagicMock (spec = httpx .Response )
636+ mock_response .status_code = 200
637+ mock_response .json .return_value = mock_api_resp_structure
638+
639+ mock_client_instance = MockAsyncClient .return_value .__aenter__ .return_value
640+ mock_client_instance .post = AsyncMock (return_value = mock_response )
641+
642+ with self .assertRaisesRegex (LLMInterfaceError , "Mistral LLM response was not valid JSON" ):
643+ await llm_interface .get_llm_action_json ([{"role" : "user" , "content" : "tap" }])
644+ # --- End Mistral API Call Tests ---
645+
522646 # --- Bedrock API Call Tests (current mock) ---
523647 async def test_get_llm_action_json_bedrock_mock_response (self ):
524648 """Test Bedrock provider path (which currently returns a mock response)."""
0 commit comments