diff --git a/test_dir/test_gemini_model_advanced.py b/test_dir/test_gemini_model_advanced.py new file mode 100644 index 0000000..29d9785 --- /dev/null +++ b/test_dir/test_gemini_model_advanced.py @@ -0,0 +1,324 @@ +""" +Tests specifically for the GeminiModel class targeting advanced scenarios and edge cases +to improve code coverage on complex methods like generate(). +""" + +import os +import json +import sys +from unittest.mock import patch, MagicMock, mock_open, call, ANY +import pytest + +# Check if running in CI +IN_CI = os.environ.get('CI', 'false').lower() == 'true' + +# Handle imports +try: + from cli_code.models.gemini import GeminiModel, MAX_AGENT_ITERATIONS + from rich.console import Console + import google.generativeai as genai + IMPORTS_AVAILABLE = True +except ImportError: + IMPORTS_AVAILABLE = False + # Create dummy classes for type checking + GeminiModel = MagicMock + Console = MagicMock + genai = MagicMock + MAX_AGENT_ITERATIONS = 10 + +# Set up conditional skipping +SHOULD_SKIP_TESTS = not IMPORTS_AVAILABLE and not IN_CI +SKIP_REASON = "Required imports not available and not in CI" + + +@pytest.mark.skipif(SHOULD_SKIP_TESTS, reason=SKIP_REASON) +class TestGeminiModelAdvanced: + """Test suite for GeminiModel class focusing on complex methods and edge cases.""" + + def setup_method(self): + """Set up test fixtures.""" + # Mock genai module + self.genai_configure_patch = patch('google.generativeai.configure') + self.mock_genai_configure = self.genai_configure_patch.start() + + self.genai_model_patch = patch('google.generativeai.GenerativeModel') + self.mock_genai_model_class = self.genai_model_patch.start() + self.mock_model_instance = MagicMock() + self.mock_genai_model_class.return_value = self.mock_model_instance + + # Mock console + self.mock_console = MagicMock(spec=Console) + + # Mock tool-related components + self.get_tool_patch = patch('cli_code.models.gemini.get_tool') + self.mock_get_tool = self.get_tool_patch.start() + + # Default tool mock + self.mock_tool = MagicMock() + self.mock_tool.execute.return_value = "Tool execution result" + self.mock_get_tool.return_value = self.mock_tool + + # Mock initial context method to avoid complexity + self.get_initial_context_patch = patch.object( + GeminiModel, '_get_initial_context', return_value="Initial context") + self.mock_get_initial_context = self.get_initial_context_patch.start() + + # Create model instance + self.model = GeminiModel("fake-api-key", self.mock_console, "gemini-2.5-pro-exp-03-25") + + def teardown_method(self): + """Tear down test fixtures.""" + self.genai_configure_patch.stop() + self.genai_model_patch.stop() + self.get_tool_patch.stop() + self.get_initial_context_patch.stop() + + def test_generate_command_handling(self): + """Test command handling in generate method.""" + # Test /exit command + result = self.model.generate("/exit") + assert result is None + + # Test /help command + result = self.model.generate("/help") + assert "Commands available" in result + + def test_generate_with_text_response(self): + """Test generate method with a simple text response.""" + # Mock the LLM response to return a simple text + mock_response = MagicMock() + mock_candidate = MagicMock() + mock_content = MagicMock() + mock_text_part = MagicMock() + + mock_text_part.text = "This is a simple text response." + mock_content.parts = [mock_text_part] + mock_candidate.content = mock_content + mock_response.candidates = [mock_candidate] + + self.mock_model_instance.generate_content.return_value = mock_response + + # Call generate + result = self.model.generate("Tell me something interesting") + + # Verify calls + self.mock_model_instance.generate_content.assert_called_once() + assert "This is a simple text response." in result + + def test_generate_with_function_call(self): + """Test generate method with a function call response.""" + # Set up mock response with function call + mock_response = MagicMock() + mock_candidate = MagicMock() + mock_content = MagicMock() + + # Create function call part + mock_function_part = MagicMock() + mock_function_part.text = None + mock_function_part.function_call = MagicMock() + mock_function_part.function_call.name = "ls" + mock_function_part.function_call.args = {"dir": "."} + + # Create text part for after function execution + mock_text_part = MagicMock() + mock_text_part.text = "Here are the directory contents." + + mock_content.parts = [mock_function_part, mock_text_part] + mock_candidate.content = mock_content + mock_response.candidates = [mock_candidate] + + # Set initial response + self.mock_model_instance.generate_content.return_value = mock_response + + # Create a second response for after function execution + mock_response2 = MagicMock() + mock_candidate2 = MagicMock() + mock_content2 = MagicMock() + mock_text_part2 = MagicMock() + + mock_text_part2.text = "Function executed successfully. Here's the result." + mock_content2.parts = [mock_text_part2] + mock_candidate2.content = mock_content2 + mock_response2.candidates = [mock_candidate2] + + # Set up mock to return different responses on successive calls + self.mock_model_instance.generate_content.side_effect = [mock_response, mock_response2] + + # Call generate + result = self.model.generate("List the files in this directory") + + # Verify tool was looked up and executed + self.mock_get_tool.assert_called_with("ls") + self.mock_tool.execute.assert_called_once() + + # Verify final response + assert "Function executed successfully" in result + + def test_generate_task_complete_tool(self): + """Test generate method with task_complete tool call.""" + # Set up mock response with task_complete function call + mock_response = MagicMock() + mock_candidate = MagicMock() + mock_content = MagicMock() + + # Create function call part + mock_function_part = MagicMock() + mock_function_part.text = None + mock_function_part.function_call = MagicMock() + mock_function_part.function_call.name = "task_complete" + mock_function_part.function_call.args = {"summary": "Task completed successfully!"} + + mock_content.parts = [mock_function_part] + mock_candidate.content = mock_content + mock_response.candidates = [mock_candidate] + + # Set the response + self.mock_model_instance.generate_content.return_value = mock_response + + # Call generate + result = self.model.generate("Complete this task") + + # Verify result contains the summary + assert "Task completed successfully!" in result + + def test_generate_with_empty_candidates(self): + """Test generate method with empty candidates response.""" + # Mock response with no candidates + mock_response = MagicMock() + mock_response.candidates = [] + + self.mock_model_instance.generate_content.return_value = mock_response + + # Call generate + result = self.model.generate("Generate something") + + # Verify error handling + assert "(Agent received response with no candidates)" in result + + def test_generate_with_empty_content(self): + """Test generate method with empty content in candidate.""" + # Mock response with empty content + mock_response = MagicMock() + mock_candidate = MagicMock() + mock_candidate.content = None + mock_response.candidates = [mock_candidate] + + self.mock_model_instance.generate_content.return_value = mock_response + + # Call generate + result = self.model.generate("Generate something") + + # Verify error handling + assert "(Agent received response candidate with no content/parts)" in result + + def test_generate_with_api_error(self): + """Test generate method when API throws an error.""" + # Mock API error + api_error_message = "API Error" + self.mock_model_instance.generate_content.side_effect = Exception(api_error_message) + + # Call generate + result = self.model.generate("Generate something") + + # Verify error handling with specific assertions + assert "Error calling Gemini API:" in result + assert api_error_message in result + + def test_generate_max_iterations(self): + """Test generate method with maximum iterations reached.""" + # Set up a response that will always include a function call, forcing iterations + mock_response = MagicMock() + mock_candidate = MagicMock() + mock_content = MagicMock() + + # Create function call part + mock_function_part = MagicMock() + mock_function_part.text = None + mock_function_part.function_call = MagicMock() + mock_function_part.function_call.name = "ls" + mock_function_part.function_call.args = {"dir": "."} + + mock_content.parts = [mock_function_part] + mock_candidate.content = mock_content + mock_response.candidates = [mock_candidate] + + # Make the model always return a function call + self.mock_model_instance.generate_content.return_value = mock_response + + # Call generate + result = self.model.generate("List files recursively") + + # Verify we hit the max iterations + assert self.mock_model_instance.generate_content.call_count <= MAX_AGENT_ITERATIONS + 1 + assert "Maximum iterations reached" in result + + def test_generate_with_multiple_tools_per_response(self): + """Test generate method with multiple tool calls in a single response.""" + # Set up mock response with multiple function calls + mock_response = MagicMock() + mock_candidate = MagicMock() + mock_content = MagicMock() + + # Create first function call part + mock_function_part1 = MagicMock() + mock_function_part1.text = None + mock_function_part1.function_call = MagicMock() + mock_function_part1.function_call.name = "ls" + mock_function_part1.function_call.args = {"dir": "."} + + # Create second function call part + mock_function_part2 = MagicMock() + mock_function_part2.text = None + mock_function_part2.function_call = MagicMock() + mock_function_part2.function_call.name = "view" + mock_function_part2.function_call.args = {"file_path": "file.txt"} + + # Create text part + mock_text_part = MagicMock() + mock_text_part.text = "Here are the results." + + mock_content.parts = [mock_function_part1, mock_function_part2, mock_text_part] + mock_candidate.content = mock_content + mock_response.candidates = [mock_candidate] + + # Set up second response for after function execution + mock_response2 = MagicMock() + mock_candidate2 = MagicMock() + mock_content2 = MagicMock() + mock_text_part2 = MagicMock() + + mock_text_part2.text = "All functions executed." + mock_content2.parts = [mock_text_part2] + mock_candidate2.content = mock_content2 + mock_response2.candidates = [mock_candidate2] + + # Set up mock to return different responses + self.mock_model_instance.generate_content.side_effect = [mock_response, mock_response2] + + # Call generate + result = self.model.generate("List files and view a file") + + # Verify only the first function is executed (since we only process one per turn) + self.mock_get_tool.assert_called_with("ls") + self.mock_tool.execute.assert_called_once_with() # Verify no arguments are passed + + def test_manage_context_window_truncation(self): + """Test specific context window management truncation with many messages.""" + # Add many messages to history + for i in range(40): # More than MAX_HISTORY_TURNS + self.model.add_to_history({"role": "user", "parts": [f"Test message {i}"]}) + self.model.add_to_history({"role": "model", "parts": [f"Test response {i}"]}) + + # Record length before management + initial_length = len(self.model.history) + + # Call the management function + self.model._manage_context_window() + + # Verify truncation occurred + assert len(self.model.history) < initial_length + + # Verify the first message is still the system prompt with specific content check + assert "System Prompt" in str(self.model.history[0]) + assert "function calling capabilities" in str(self.model.history[0]) + assert "CLI-Code" in str(self.model.history[0]) \ No newline at end of file diff --git a/test_dir/test_ollama_model_advanced.py b/test_dir/test_ollama_model_advanced.py new file mode 100644 index 0000000..ea20752 --- /dev/null +++ b/test_dir/test_ollama_model_advanced.py @@ -0,0 +1,452 @@ +""" +Tests specifically for the OllamaModel class targeting advanced scenarios and edge cases +to improve code coverage on complex methods like generate(). +""" + +import os +import json +import sys +from unittest.mock import patch, MagicMock, mock_open, call, ANY +import pytest + +# Check if running in CI +IN_CI = os.environ.get('CI', 'false').lower() == 'true' + +# Handle imports +try: + from cli_code.models.ollama import OllamaModel, MAX_OLLAMA_ITERATIONS + from rich.console import Console + IMPORTS_AVAILABLE = True +except ImportError: + IMPORTS_AVAILABLE = False + # Create dummy classes for type checking + OllamaModel = MagicMock + Console = MagicMock + MAX_OLLAMA_ITERATIONS = 5 + +# Set up conditional skipping +SHOULD_SKIP_TESTS = not IMPORTS_AVAILABLE and not IN_CI +SKIP_REASON = "Required imports not available and not in CI" + + +@pytest.mark.skipif(SHOULD_SKIP_TESTS, reason=SKIP_REASON) +class TestOllamaModelAdvanced: + """Test suite for OllamaModel class focusing on complex methods and edge cases.""" + + def setup_method(self): + """Set up test fixtures.""" + # Mock OpenAI module + self.openai_patch = patch('cli_code.models.ollama.OpenAI') + self.mock_openai = self.openai_patch.start() + + # Mock the OpenAI client instance + self.mock_client = MagicMock() + self.mock_openai.return_value = self.mock_client + + # Mock console + self.mock_console = MagicMock(spec=Console) + + # Mock tool-related components + self.get_tool_patch = patch('cli_code.models.ollama.get_tool') + self.mock_get_tool = self.get_tool_patch.start() + + # Default tool mock + self.mock_tool = MagicMock() + self.mock_tool.execute.return_value = "Tool execution result" + self.mock_get_tool.return_value = self.mock_tool + + # Mock initial context method to avoid complexity + self.get_initial_context_patch = patch.object( + OllamaModel, '_get_initial_context', return_value="Initial context") + self.mock_get_initial_context = self.get_initial_context_patch.start() + + # Set up mock for JSON loads + self.json_loads_patch = patch('json.loads') + self.mock_json_loads = self.json_loads_patch.start() + + # Mock questionary for user confirmations + self.questionary_patch = patch('questionary.confirm') + self.mock_questionary = self.questionary_patch.start() + self.mock_questionary_confirm = MagicMock() + self.mock_questionary.return_value = self.mock_questionary_confirm + self.mock_questionary_confirm.ask.return_value = True # Default to confirmed + + # Create model instance + self.model = OllamaModel("http://localhost:11434", self.mock_console, "llama3") + + def teardown_method(self): + """Tear down test fixtures.""" + self.openai_patch.stop() + self.get_tool_patch.stop() + self.get_initial_context_patch.stop() + self.json_loads_patch.stop() + self.questionary_patch.stop() + + def test_generate_with_text_response(self): + """Test generate method with a simple text response.""" + # Mock chat completions response with text + mock_message = MagicMock() + mock_message.content = "This is a simple text response." + mock_message.tool_calls = None + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + self.mock_client.chat.completions.create.return_value = mock_response + + # Call generate + result = self.model.generate("Tell me something interesting") + + # Verify API was called correctly + self.mock_client.chat.completions.create.assert_called_once() + call_kwargs = self.mock_client.chat.completions.create.call_args[1] + assert call_kwargs["model"] == "llama3" + + # Verify result + assert result == "This is a simple text response." + + def test_generate_with_tool_call(self): + """Test generate method with a tool call response.""" + # Mock a tool call in the response + mock_tool_call = MagicMock() + mock_tool_call.id = "call123" + mock_tool_call.function.name = "ls" + mock_tool_call.function.arguments = '{"dir": "."}' + + # Parse the arguments as expected + self.mock_json_loads.return_value = {"dir": "."} + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [mock_tool_call] + mock_message.model_dump.return_value = {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "ls", "arguments": '{"dir": "."}'}}]} + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + # Set up initial response + self.mock_client.chat.completions.create.return_value = mock_response + + # Create a second response for after tool execution + mock_message2 = MagicMock() + mock_message2.content = "Tool executed successfully." + mock_message2.tool_calls = None + + mock_choice2 = MagicMock() + mock_choice2.message = mock_message2 + + mock_response2 = MagicMock() + mock_response2.choices = [mock_choice2] + + # Set up successive responses + self.mock_client.chat.completions.create.side_effect = [mock_response, mock_response2] + + # Call generate + result = self.model.generate("List the files in this directory") + + # Verify tool was called + self.mock_get_tool.assert_called_with("ls") + self.mock_tool.execute.assert_called_once() + + assert result == "Tool executed successfully." + # Example of a more specific assertion + # assert "Tool executed successfully" in result and "ls" in result + + def test_generate_with_task_complete_tool(self): + """Test generate method with task_complete tool.""" + # Mock a task_complete tool call + mock_tool_call = MagicMock() + mock_tool_call.id = "call123" + mock_tool_call.function.name = "task_complete" + mock_tool_call.function.arguments = '{"summary": "Task completed successfully!"}' + + # Parse the arguments as expected + self.mock_json_loads.return_value = {"summary": "Task completed successfully!"} + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [mock_tool_call] + mock_message.model_dump.return_value = {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "task_complete", "arguments": '{"summary": "Task completed successfully!"}'}}]} + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + self.mock_client.chat.completions.create.return_value = mock_response + + # Call generate + result = self.model.generate("Complete this task") + + # Verify result contains the summary + assert result == "Task completed successfully!" + + def test_generate_with_sensitive_tool_approved(self): + """Test generate method with sensitive tool that requires approval.""" + # Mock a sensitive tool call (edit) + mock_tool_call = MagicMock() + mock_tool_call.id = "call123" + mock_tool_call.function.name = "edit" + mock_tool_call.function.arguments = '{"file_path": "file.txt", "content": "new content"}' + + # Parse the arguments as expected + self.mock_json_loads.return_value = {"file_path": "file.txt", "content": "new content"} + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [mock_tool_call] + mock_message.model_dump.return_value = {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "edit", "arguments": '{"file_path": "file.txt", "content": "new content"}'}}]} + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + # Set up confirmation to be approved + self.mock_questionary_confirm.ask.return_value = True + + # Set up initial response + self.mock_client.chat.completions.create.return_value = mock_response + + # Create a second response for after tool execution + mock_message2 = MagicMock() + mock_message2.content = "Edit completed." + mock_message2.tool_calls = None + + mock_choice2 = MagicMock() + mock_choice2.message = mock_message2 + + mock_response2 = MagicMock() + mock_response2.choices = [mock_choice2] + + # Set up successive responses + self.mock_client.chat.completions.create.side_effect = [mock_response, mock_response2] + + # Call generate + result = self.model.generate("Edit this file") + + # Verify user was asked for confirmation + self.mock_questionary_confirm.ask.assert_called_once() + + # Verify tool was called after approval + self.mock_get_tool.assert_called_with("edit") + self.mock_tool.execute.assert_called_once() + + # Verify result + assert result == "Edit completed." + + def test_generate_with_sensitive_tool_rejected(self): + """Test generate method with sensitive tool that is rejected.""" + # Mock a sensitive tool call (edit) + mock_tool_call = MagicMock() + mock_tool_call.id = "call123" + mock_tool_call.function.name = "edit" + mock_tool_call.function.arguments = '{"file_path": "file.txt", "content": "new content"}' + + # Parse the arguments as expected + self.mock_json_loads.return_value = {"file_path": "file.txt", "content": "new content"} + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [mock_tool_call] + mock_message.model_dump.return_value = {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "edit", "arguments": '{"file_path": "file.txt", "content": "new content"}'}}]} + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + # Set up confirmation to be rejected + self.mock_questionary_confirm.ask.return_value = False + + # Set up initial response + self.mock_client.chat.completions.create.return_value = mock_response + + # Create a second response for after rejection + mock_message2 = MagicMock() + mock_message2.content = "I'll find another approach." + mock_message2.tool_calls = None + + mock_choice2 = MagicMock() + mock_choice2.message = mock_message2 + + mock_response2 = MagicMock() + mock_response2.choices = [mock_choice2] + + # Set up successive responses + self.mock_client.chat.completions.create.side_effect = [mock_response, mock_response2] + + # Call generate + result = self.model.generate("Edit this file") + + # Verify user was asked for confirmation + self.mock_questionary_confirm.ask.assert_called_once() + + # Verify tool was NOT called after rejection + self.mock_tool.execute.assert_not_called() + + # Verify result + assert result == "I'll find another approach." + + def test_generate_with_api_error(self): + """Test generate method with API error.""" + # Mock API error + self.mock_client.chat.completions.create.side_effect = Exception("API Error") + + # Call generate + result = self.model.generate("Generate something") + + # Verify error handling + assert "Error calling Ollama API:" in result + # Example of a more specific assertion + # assert result == "Error calling Ollama API: API Error" + + def test_generate_max_iterations(self): + """Test generate method with maximum iterations reached.""" + # Mock a tool call that will keep being returned + mock_tool_call = MagicMock() + mock_tool_call.id = "call123" + mock_tool_call.function.name = "ls" + mock_tool_call.function.arguments = '{"dir": "."}' + + # Parse the arguments as expected + self.mock_json_loads.return_value = {"dir": "."} + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [mock_tool_call] + mock_message.model_dump.return_value = {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "ls", "arguments": '{"dir": "."}'}}]} + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + # Always return the same response with a tool call to force iteration + self.mock_client.chat.completions.create.return_value = mock_response + + # Call generate + result = self.model.generate("List files recursively") + + # Verify max iterations were handled + assert self.mock_client.chat.completions.create.call_count <= MAX_OLLAMA_ITERATIONS + 1 + assert "Maximum iterations" in result + + def test_manage_ollama_context(self): + """Test context window management for Ollama.""" + # Add many messages to history + for i in range(30): # Many more than fits in context + self.model.add_to_history({"role": "user", "content": f"Message {i}"}) + self.model.add_to_history({"role": "assistant", "content": f"Response {i}"}) + + # Record history length before management + initial_length = len(self.model.history) + + # Manage context + self.model._manage_ollama_context() + + # Verify truncation + assert len(self.model.history) < initial_length + + # Verify system prompt is preserved with specific content check + assert self.model.history[0]["role"] == "system" + # Example of a more specific assertion + # assert self.model.history[0]["content"] == "You are a helpful AI coding assistant..." + assert "You are a helpful AI coding assistant" in self.model.history[0]["content"] + assert "function calling capabilities" in self.model.history[0]["content"] + + def test_generate_with_token_counting(self): + """Test generate method with token counting and context management.""" + # Mock token counting to simulate context window being exceeded + with patch('cli_code.models.ollama.count_tokens') as mock_count_tokens: + # Set up a high token count to trigger context management + mock_count_tokens.return_value = 10000 # Above context limit + + # Set up a basic response + mock_message = MagicMock() + mock_message.content = "Response after context management" + mock_message.tool_calls = None + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + self.mock_client.chat.completions.create.return_value = mock_response + + # Call generate + result = self.model.generate("Generate with large context") + + # Verify token counting was used + mock_count_tokens.assert_called() + + # Verify result + assert result == "Response after context management" + + def test_error_handling_for_tool_execution(self): + """Test error handling during tool execution.""" + # Mock a tool call + mock_tool_call = MagicMock() + mock_tool_call.id = "call123" + mock_tool_call.function.name = "ls" + mock_tool_call.function.arguments = '{"dir": "."}' + + # Parse the arguments as expected + self.mock_json_loads.return_value = {"dir": "."} + + mock_message = MagicMock() + mock_message.content = None + mock_message.tool_calls = [mock_tool_call] + mock_message.model_dump.return_value = {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "ls", "arguments": '{"dir": "."}'}}]} + + mock_choice = MagicMock() + mock_choice.message = mock_message + + mock_response = MagicMock() + mock_response.choices = [mock_choice] + + # Set up initial response + self.mock_client.chat.completions.create.return_value = mock_response + + # Make tool execution fail + error_message = "Tool execution failed" + self.mock_tool.execute.side_effect = Exception(error_message) + + # Create a second response for after tool failure + mock_message2 = MagicMock() + mock_message2.content = "I encountered an error." + mock_message2.tool_calls = None + + mock_choice2 = MagicMock() + mock_choice2.message = mock_message2 + + mock_response2 = MagicMock() + mock_response2.choices = [mock_choice2] + + # Set up successive responses + self.mock_client.chat.completions.create.side_effect = [mock_response, mock_response2] + + # Call generate + result = self.model.generate("List the files") + + # Verify error was handled gracefully with specific assertions + assert result == "I encountered an error." + # Verify that error details were added to history + error_found = False + for message in self.model.history: + if message.get("role") == "tool" and message.get("name") == "ls": + assert "error" in message.get("content", "").lower() + assert error_message in message.get("content", "") + error_found = True + assert error_found, "Error message not found in history" \ No newline at end of file