Skip to content
This repository was archived by the owner on Apr 23, 2025. It is now read-only.

Commit 1a59c15

Browse files
authored
Merge pull request #2 from BlueCentre/improve-model-test-coverage
Add advanced test cases for Gemini and Ollama models to improve code coverage
2 parents 9a19b43 + fdea869 commit 1a59c15

2 files changed

Lines changed: 776 additions & 0 deletions

File tree

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
"""
2+
Tests specifically for the GeminiModel class targeting advanced scenarios and edge cases
3+
to improve code coverage on complex methods like generate().
4+
"""
5+
6+
import os
7+
import json
8+
import sys
9+
from unittest.mock import patch, MagicMock, mock_open, call, ANY
10+
import pytest
11+
12+
# Check if running in CI
13+
IN_CI = os.environ.get('CI', 'false').lower() == 'true'
14+
15+
# Handle imports
16+
try:
17+
from cli_code.models.gemini import GeminiModel, MAX_AGENT_ITERATIONS
18+
from rich.console import Console
19+
import google.generativeai as genai
20+
IMPORTS_AVAILABLE = True
21+
except ImportError:
22+
IMPORTS_AVAILABLE = False
23+
# Create dummy classes for type checking
24+
GeminiModel = MagicMock
25+
Console = MagicMock
26+
genai = MagicMock
27+
MAX_AGENT_ITERATIONS = 10
28+
29+
# Set up conditional skipping
30+
SHOULD_SKIP_TESTS = not IMPORTS_AVAILABLE and not IN_CI
31+
SKIP_REASON = "Required imports not available and not in CI"
32+
33+
34+
@pytest.mark.skipif(SHOULD_SKIP_TESTS, reason=SKIP_REASON)
35+
class TestGeminiModelAdvanced:
36+
"""Test suite for GeminiModel class focusing on complex methods and edge cases."""
37+
38+
def setup_method(self):
39+
"""Set up test fixtures."""
40+
# Mock genai module
41+
self.genai_configure_patch = patch('google.generativeai.configure')
42+
self.mock_genai_configure = self.genai_configure_patch.start()
43+
44+
self.genai_model_patch = patch('google.generativeai.GenerativeModel')
45+
self.mock_genai_model_class = self.genai_model_patch.start()
46+
self.mock_model_instance = MagicMock()
47+
self.mock_genai_model_class.return_value = self.mock_model_instance
48+
49+
# Mock console
50+
self.mock_console = MagicMock(spec=Console)
51+
52+
# Mock tool-related components
53+
self.get_tool_patch = patch('cli_code.models.gemini.get_tool')
54+
self.mock_get_tool = self.get_tool_patch.start()
55+
56+
# Default tool mock
57+
self.mock_tool = MagicMock()
58+
self.mock_tool.execute.return_value = "Tool execution result"
59+
self.mock_get_tool.return_value = self.mock_tool
60+
61+
# Mock initial context method to avoid complexity
62+
self.get_initial_context_patch = patch.object(
63+
GeminiModel, '_get_initial_context', return_value="Initial context")
64+
self.mock_get_initial_context = self.get_initial_context_patch.start()
65+
66+
# Create model instance
67+
self.model = GeminiModel("fake-api-key", self.mock_console, "gemini-2.5-pro-exp-03-25")
68+
69+
def teardown_method(self):
70+
"""Tear down test fixtures."""
71+
self.genai_configure_patch.stop()
72+
self.genai_model_patch.stop()
73+
self.get_tool_patch.stop()
74+
self.get_initial_context_patch.stop()
75+
76+
def test_generate_command_handling(self):
77+
"""Test command handling in generate method."""
78+
# Test /exit command
79+
result = self.model.generate("/exit")
80+
assert result is None
81+
82+
# Test /help command
83+
result = self.model.generate("/help")
84+
assert "Commands available" in result
85+
86+
def test_generate_with_text_response(self):
87+
"""Test generate method with a simple text response."""
88+
# Mock the LLM response to return a simple text
89+
mock_response = MagicMock()
90+
mock_candidate = MagicMock()
91+
mock_content = MagicMock()
92+
mock_text_part = MagicMock()
93+
94+
mock_text_part.text = "This is a simple text response."
95+
mock_content.parts = [mock_text_part]
96+
mock_candidate.content = mock_content
97+
mock_response.candidates = [mock_candidate]
98+
99+
self.mock_model_instance.generate_content.return_value = mock_response
100+
101+
# Call generate
102+
result = self.model.generate("Tell me something interesting")
103+
104+
# Verify calls
105+
self.mock_model_instance.generate_content.assert_called_once()
106+
assert "This is a simple text response." in result
107+
108+
def test_generate_with_function_call(self):
109+
"""Test generate method with a function call response."""
110+
# Set up mock response with function call
111+
mock_response = MagicMock()
112+
mock_candidate = MagicMock()
113+
mock_content = MagicMock()
114+
115+
# Create function call part
116+
mock_function_part = MagicMock()
117+
mock_function_part.text = None
118+
mock_function_part.function_call = MagicMock()
119+
mock_function_part.function_call.name = "ls"
120+
mock_function_part.function_call.args = {"dir": "."}
121+
122+
# Create text part for after function execution
123+
mock_text_part = MagicMock()
124+
mock_text_part.text = "Here are the directory contents."
125+
126+
mock_content.parts = [mock_function_part, mock_text_part]
127+
mock_candidate.content = mock_content
128+
mock_response.candidates = [mock_candidate]
129+
130+
# Set initial response
131+
self.mock_model_instance.generate_content.return_value = mock_response
132+
133+
# Create a second response for after function execution
134+
mock_response2 = MagicMock()
135+
mock_candidate2 = MagicMock()
136+
mock_content2 = MagicMock()
137+
mock_text_part2 = MagicMock()
138+
139+
mock_text_part2.text = "Function executed successfully. Here's the result."
140+
mock_content2.parts = [mock_text_part2]
141+
mock_candidate2.content = mock_content2
142+
mock_response2.candidates = [mock_candidate2]
143+
144+
# Set up mock to return different responses on successive calls
145+
self.mock_model_instance.generate_content.side_effect = [mock_response, mock_response2]
146+
147+
# Call generate
148+
result = self.model.generate("List the files in this directory")
149+
150+
# Verify tool was looked up and executed
151+
self.mock_get_tool.assert_called_with("ls")
152+
self.mock_tool.execute.assert_called_once()
153+
154+
# Verify final response
155+
assert "Function executed successfully" in result
156+
157+
def test_generate_task_complete_tool(self):
158+
"""Test generate method with task_complete tool call."""
159+
# Set up mock response with task_complete function call
160+
mock_response = MagicMock()
161+
mock_candidate = MagicMock()
162+
mock_content = MagicMock()
163+
164+
# Create function call part
165+
mock_function_part = MagicMock()
166+
mock_function_part.text = None
167+
mock_function_part.function_call = MagicMock()
168+
mock_function_part.function_call.name = "task_complete"
169+
mock_function_part.function_call.args = {"summary": "Task completed successfully!"}
170+
171+
mock_content.parts = [mock_function_part]
172+
mock_candidate.content = mock_content
173+
mock_response.candidates = [mock_candidate]
174+
175+
# Set the response
176+
self.mock_model_instance.generate_content.return_value = mock_response
177+
178+
# Call generate
179+
result = self.model.generate("Complete this task")
180+
181+
# Verify result contains the summary
182+
assert "Task completed successfully!" in result
183+
184+
def test_generate_with_empty_candidates(self):
185+
"""Test generate method with empty candidates response."""
186+
# Mock response with no candidates
187+
mock_response = MagicMock()
188+
mock_response.candidates = []
189+
190+
self.mock_model_instance.generate_content.return_value = mock_response
191+
192+
# Call generate
193+
result = self.model.generate("Generate something")
194+
195+
# Verify error handling
196+
assert "(Agent received response with no candidates)" in result
197+
198+
def test_generate_with_empty_content(self):
199+
"""Test generate method with empty content in candidate."""
200+
# Mock response with empty content
201+
mock_response = MagicMock()
202+
mock_candidate = MagicMock()
203+
mock_candidate.content = None
204+
mock_response.candidates = [mock_candidate]
205+
206+
self.mock_model_instance.generate_content.return_value = mock_response
207+
208+
# Call generate
209+
result = self.model.generate("Generate something")
210+
211+
# Verify error handling
212+
assert "(Agent received response candidate with no content/parts)" in result
213+
214+
def test_generate_with_api_error(self):
215+
"""Test generate method when API throws an error."""
216+
# Mock API error
217+
api_error_message = "API Error"
218+
self.mock_model_instance.generate_content.side_effect = Exception(api_error_message)
219+
220+
# Call generate
221+
result = self.model.generate("Generate something")
222+
223+
# Verify error handling with specific assertions
224+
assert "Error calling Gemini API:" in result
225+
assert api_error_message in result
226+
227+
def test_generate_max_iterations(self):
228+
"""Test generate method with maximum iterations reached."""
229+
# Set up a response that will always include a function call, forcing iterations
230+
mock_response = MagicMock()
231+
mock_candidate = MagicMock()
232+
mock_content = MagicMock()
233+
234+
# Create function call part
235+
mock_function_part = MagicMock()
236+
mock_function_part.text = None
237+
mock_function_part.function_call = MagicMock()
238+
mock_function_part.function_call.name = "ls"
239+
mock_function_part.function_call.args = {"dir": "."}
240+
241+
mock_content.parts = [mock_function_part]
242+
mock_candidate.content = mock_content
243+
mock_response.candidates = [mock_candidate]
244+
245+
# Make the model always return a function call
246+
self.mock_model_instance.generate_content.return_value = mock_response
247+
248+
# Call generate
249+
result = self.model.generate("List files recursively")
250+
251+
# Verify we hit the max iterations
252+
assert self.mock_model_instance.generate_content.call_count <= MAX_AGENT_ITERATIONS + 1
253+
assert "Maximum iterations reached" in result
254+
255+
def test_generate_with_multiple_tools_per_response(self):
256+
"""Test generate method with multiple tool calls in a single response."""
257+
# Set up mock response with multiple function calls
258+
mock_response = MagicMock()
259+
mock_candidate = MagicMock()
260+
mock_content = MagicMock()
261+
262+
# Create first function call part
263+
mock_function_part1 = MagicMock()
264+
mock_function_part1.text = None
265+
mock_function_part1.function_call = MagicMock()
266+
mock_function_part1.function_call.name = "ls"
267+
mock_function_part1.function_call.args = {"dir": "."}
268+
269+
# Create second function call part
270+
mock_function_part2 = MagicMock()
271+
mock_function_part2.text = None
272+
mock_function_part2.function_call = MagicMock()
273+
mock_function_part2.function_call.name = "view"
274+
mock_function_part2.function_call.args = {"file_path": "file.txt"}
275+
276+
# Create text part
277+
mock_text_part = MagicMock()
278+
mock_text_part.text = "Here are the results."
279+
280+
mock_content.parts = [mock_function_part1, mock_function_part2, mock_text_part]
281+
mock_candidate.content = mock_content
282+
mock_response.candidates = [mock_candidate]
283+
284+
# Set up second response for after function execution
285+
mock_response2 = MagicMock()
286+
mock_candidate2 = MagicMock()
287+
mock_content2 = MagicMock()
288+
mock_text_part2 = MagicMock()
289+
290+
mock_text_part2.text = "All functions executed."
291+
mock_content2.parts = [mock_text_part2]
292+
mock_candidate2.content = mock_content2
293+
mock_response2.candidates = [mock_candidate2]
294+
295+
# Set up mock to return different responses
296+
self.mock_model_instance.generate_content.side_effect = [mock_response, mock_response2]
297+
298+
# Call generate
299+
result = self.model.generate("List files and view a file")
300+
301+
# Verify only the first function is executed (since we only process one per turn)
302+
self.mock_get_tool.assert_called_with("ls")
303+
self.mock_tool.execute.assert_called_once_with() # Verify no arguments are passed
304+
305+
def test_manage_context_window_truncation(self):
306+
"""Test specific context window management truncation with many messages."""
307+
# Add many messages to history
308+
for i in range(40): # More than MAX_HISTORY_TURNS
309+
self.model.add_to_history({"role": "user", "parts": [f"Test message {i}"]})
310+
self.model.add_to_history({"role": "model", "parts": [f"Test response {i}"]})
311+
312+
# Record length before management
313+
initial_length = len(self.model.history)
314+
315+
# Call the management function
316+
self.model._manage_context_window()
317+
318+
# Verify truncation occurred
319+
assert len(self.model.history) < initial_length
320+
321+
# Verify the first message is still the system prompt with specific content check
322+
assert "System Prompt" in str(self.model.history[0])
323+
assert "function calling capabilities" in str(self.model.history[0])
324+
assert "CLI-Code" in str(self.model.history[0])

0 commit comments

Comments
 (0)