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