11"""Comprehensive unit tests for TaskHandler using mocks."""
22
3+ from __future__ import annotations
4+
35import json
4- import os
56import sys
67import unittest
8+ from pathlib import Path
9+ from typing import Any
710from unittest .mock import MagicMock
811
912from mcp import McpError
1013
1114# Add parent directory to path for imports
12- sys .path .insert (0 , os . path . join ( os . path . dirname (__file__ ), ".." , ".." ))
15+ sys .path .insert (0 , str ( Path (__file__ ). parent . parent . parent ))
1316
1417# Mock the mcp module before importing TaskHandler
1518sys .modules ["mcp" ] = MagicMock ()
1619sys .modules ["mcp" ].McpError = Exception
1720
18- import pytest
21+ import pytest # noqa: E402
1922
20- from handlers .task_getter_github import TaskGitHubIssue
21- from handlers .task_getter_gitlab import TaskGitLabIssue
22- from handlers .task_handler import TaskHandler
23- from tests .mocks .mock_llm_client import (
23+ from handlers .task_getter_github import TaskGitHubIssue # noqa: E402
24+ from handlers .task_getter_gitlab import TaskGitLabIssue # noqa: E402
25+ from handlers .task_handler import TaskHandler # noqa: E402
26+ from tests .mocks .mock_llm_client import ( # noqa: E402
2427 MockLLMClient ,
2528 MockLLMClientWithErrors ,
2629 MockLLMClientWithToolCalls ,
2730)
28- from tests .mocks .mock_mcp_client import MockMCPToolClient
31+ from tests .mocks .mock_mcp_client import MockMCPToolClient # noqa: E402
32+
33+ # Constants
34+ MAX_TOOL_FAILURES = 2
2935
3036
3137class TestTaskHandler (unittest .TestCase ):
@@ -75,9 +81,9 @@ def test_task_handler_creation(self) -> None:
7581 config = self .config ,
7682 )
7783
78- assert task_handler .llm_client is not None
79- assert task_handler .mcp_clients is not None
80- assert task_handler .config is not None
84+ assert task_handler .llm_client is not None # noqa: S101
85+ assert task_handler .mcp_clients is not None # noqa: S101
86+ assert task_handler .config is not None # noqa: S101
8187
8288 def test_sanitize_arguments_dict (self ) -> None :
8389 """Test argument sanitization with dict input."""
@@ -90,7 +96,7 @@ def test_sanitize_arguments_dict(self) -> None:
9096 # Test with valid dict
9197 args_dict = {"owner" : "testorg" , "repo" : "testrepo" , "issue_number" : 1 }
9298 sanitized = task_handler .sanitize_arguments (args_dict )
93- assert sanitized == args_dict
99+ assert sanitized == args_dict # noqa: S101
94100
95101 def test_sanitize_arguments_json_string (self ) -> None :
96102 """Test argument sanitization with JSON string input."""
@@ -104,7 +110,7 @@ def test_sanitize_arguments_json_string(self) -> None:
104110 args_json = '{"owner": "testorg", "repo": "testrepo", "issue_number": 1}'
105111 sanitized = task_handler .sanitize_arguments (args_json )
106112 expected = {"owner" : "testorg" , "repo" : "testrepo" , "issue_number" : 1 }
107- assert sanitized == expected
113+ assert sanitized == expected # noqa: S101
108114
109115 def test_sanitize_arguments_invalid_json (self ) -> None :
110116 """Test argument sanitization with invalid JSON."""
@@ -115,7 +121,7 @@ def test_sanitize_arguments_invalid_json(self) -> None:
115121 )
116122
117123 # Test with invalid JSON
118- with pytest .raises (ValueError ):
124+ with pytest .raises (ValueError , match = "JSON decode error" ):
119125 task_handler .sanitize_arguments ('{"invalid": json}' )
120126
121127 def test_sanitize_arguments_invalid_type (self ) -> None :
@@ -145,7 +151,7 @@ def test_handle_task_basic_workflow(self) -> None:
145151 result = task_handler .handle (self .github_task )
146152
147153 # Should complete without errors
148- assert result is None # handle() method returns None on completion
154+ assert result is None # handle() method returns None on completion # noqa: S101
149155
150156 def test_handle_task_with_tool_calls (self ) -> None :
151157 """Test task handling with tool calls."""
@@ -162,7 +168,7 @@ def test_handle_task_with_tool_calls(self) -> None:
162168 result = task_handler .handle (self .github_task )
163169
164170 # Should complete after making tool calls
165- assert result is None
171+ assert result is None # noqa: S101
166172
167173 def test_handle_task_with_think_tags (self ) -> None :
168174 """Test handling of <think> tags in LLM responses."""
@@ -186,7 +192,8 @@ def test_handle_task_with_think_tags(self) -> None:
186192 task_handler .handle (self .github_task )
187193
188194 # Verify that think content was posted as comment
189- # Check that comment was called with the think content (may have mention=True due to task logic)
195+ # Check that comment was called with the think content
196+ # (may have mention=True due to task logic)
190197 self .github_task .comment .assert_called ()
191198
192199 def test_handle_task_with_errors (self ) -> None :
@@ -195,14 +202,16 @@ def test_handle_task_with_errors(self) -> None:
195202 error_llm = MockLLMClientWithErrors (self .config )
196203
197204 task_handler = TaskHandler (
198- llm_client = error_llm , mcp_clients = {"github" : self .github_mcp_client }, config = self .config ,
205+ llm_client = error_llm ,
206+ mcp_clients = {"github" : self .github_mcp_client },
207+ config = self .config ,
199208 )
200209
201210 # Handle the task (should handle errors gracefully)
202211 try :
203212 task_handler .handle (self .github_task )
204213 # If it completes, that's good error handling
205- except Exception as e :
214+ except ( OSError , RuntimeError ) as e :
206215 # Should not raise unhandled exceptions
207216 self .fail (f"TaskHandler should handle errors gracefully, but got: { e } " )
208217
@@ -229,7 +238,7 @@ def test_handle_task_max_iterations(self) -> None:
229238 result = task_handler .handle (self .github_task )
230239
231240 # Should stop due to max iterations
232- assert result is None
241+ assert result is None # noqa: S101
233242
234243 def test_handle_task_with_invalid_json_responses (self ) -> None :
235244 """Test handling of invalid JSON responses from LLM."""
@@ -251,7 +260,7 @@ def test_handle_task_with_invalid_json_responses(self) -> None:
251260 try :
252261 task_handler .handle (self .github_task )
253262 # Should eventually complete with valid response
254- except Exception as e :
263+ except ( OSError , RuntimeError ) as e :
255264 # Should not crash on invalid JSON
256265 self .fail (f"TaskHandler should handle invalid JSON gracefully, but got: { e } " )
257266
@@ -262,15 +271,13 @@ def test_handle_task_tool_error_management(self) -> None:
262271 call_count = 0
263272 original_call_tool = error_mcp_client .call_tool
264273
265- def failing_call_tool (tool , args ) :
274+ def failing_call_tool (tool : str , args : dict [ str , Any ]) -> dict [ str , Any ] | None :
266275 nonlocal call_count
267276 call_count += 1
268- if call_count <= 2 : # Fail first 2 calls
277+ if call_count <= MAX_TOOL_FAILURES : # Fail first 2 calls
269278 msg = "Tool call failed"
270- raise ExceptionGroup (
271- msg ,
272- [ExceptionGroup ("Tool call failed" , [McpError ("Tool call failed" )])],
273- )
279+ # Use nested exception structure for compatibility
280+ raise RuntimeError (msg ) from McpError ("Tool call failed" )
274281 return original_call_tool (tool , args )
275282
276283 error_mcp_client .call_tool = failing_call_tool
@@ -312,16 +319,14 @@ def failing_call_tool(tool, args):
312319 self .llm_client .set_custom_responses (tool_responses )
313320
314321 task_handler = TaskHandler (
315- llm_client = self .llm_client , mcp_clients = {"github" : error_mcp_client }, config = self .config ,
322+ llm_client = self .llm_client ,
323+ mcp_clients = {"github" : error_mcp_client },
324+ config = self .config ,
316325 )
317326
318327 # Handle the task
319- try :
328+ with pytest . raises (( OSError , RuntimeError ), match = "Tool call failed" ) :
320329 task_handler .handle (self .github_task )
321- # Should eventually succeed after retries
322- except Exception as e :
323- # May fail due to tool errors, which is acceptable
324- assert "Tool call failed" in str (e )
325330
326331 def test_make_system_prompt (self ) -> None :
327332 """Test system prompt generation."""
@@ -331,10 +336,10 @@ def test_make_system_prompt(self) -> None:
331336 config = self .config ,
332337 )
333338
334- # Test that system prompt is generated
335- system_prompt = task_handler ._make_system_prompt ()
336- assert isinstance (system_prompt , str )
337- assert len (system_prompt ) > 0
339+ # Test that system prompt is generated (accessing private method for testing)
340+ system_prompt = task_handler ._make_system_prompt () # noqa: SLF001
341+ assert isinstance (system_prompt , str ) # noqa: S101
342+ assert len (system_prompt ) > 0 # noqa: S101
338343
339344
340345class TestTaskHandlerWithDifferentTasks (unittest .TestCase ):
@@ -385,7 +390,7 @@ def test_handle_gitlab_task(self) -> None:
385390
386391 # Handle GitLab task
387392 result = task_handler .handle (self .gitlab_task )
388- assert result is None # Should complete successfully
393+ assert result is None # Should complete successfully # noqa: S101
389394
390395 def test_handle_task_with_multiple_mcp_clients (self ) -> None :
391396 """Test task handling with multiple MCP clients."""
@@ -405,7 +410,7 @@ def test_handle_task_with_multiple_mcp_clients(self) -> None:
405410
406411 # Handle task
407412 result = task_handler .handle (self .gitlab_task )
408- assert result is None
413+ assert result is None # noqa: S101
409414
410415
411416if __name__ == "__main__" :
0 commit comments