| import json |
| from unittest.mock import AsyncMock, MagicMock, patch |
|
|
| import pytest |
|
|
| from src.server.services.agent_service import AgentService |
|
|
|
|
| @pytest.fixture |
| def mock_mcp_client(): |
| client = AsyncMock() |
| |
| client.search_code_examples.return_value = "Found example: from foo.bar import Baz" |
| return client |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_analyze_error_with_skills_flow(mock_mcp_client): |
| """ |
| [Phase 4.7] Test the 'Look-Before-Leap' flow where DevBot uses a tool before fixing. |
| """ |
| |
| service = AgentService(mcp_client=mock_mcp_client) |
|
|
| mock_tool_call = MagicMock() |
| mock_tool_call.function.name = "search_code_examples" |
| mock_tool_call.function.arguments = '{"query": "ImportError"}' |
| mock_tool_call.id = "call_123" |
|
|
| |
| r1_message = MagicMock() |
| r1_message.tool_calls = [mock_tool_call] |
| r1_message.content = None |
| r1_choice = MagicMock() |
| r1_choice.message = r1_message |
|
|
| |
| final_fix = {"file_path": "script.py", "fixed_content": "import foo", "reasoning": "Fixed based on search"} |
| r2_message = MagicMock() |
| r2_message.tool_calls = None |
| r2_message.content = json.dumps(final_fix) |
| r2_choice = MagicMock() |
| r2_choice.message = r2_message |
|
|
| |
| mock_llm_client = AsyncMock() |
| mock_llm_client.chat.completions.create.side_effect = [ |
| MagicMock(choices=[r1_choice]), |
| MagicMock(choices=[r2_choice]), |
| ] |
|
|
| mock_ctx = AsyncMock() |
| mock_ctx.__aenter__.return_value = mock_llm_client |
|
|
| mock_provider_config = {"chat_model": "test-model-v1"} |
|
|
| with ( |
| patch("src.server.services.dev_ops_agent_service.get_llm_client", return_value=mock_ctx), |
| patch( |
| "src.server.services.dev_ops_agent_service.credential_service.get_active_provider", return_value=mock_provider_config |
| ), |
| ): |
| result = await service.dev_ops._analyze_error_with_structured_output( |
| command="python script.py", stderr="ImportError: No module named foo", agent_id="bcb00484-30bd-46fb-9e39-84b2ec4ced31" |
| ) |
|
|
| mock_mcp_client.search_code_examples.assert_called_once_with(query="ImportError") |
| assert mock_llm_client.chat.completions.create.call_count == 2 |
| assert result == final_fix |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_analyze_error_graceful_degradation(): |
| """ |
| [Phase 4.7] Test that DevBot works even if MCP client is missing (legacy mode). |
| """ |
| service = AgentService(mcp_client=None) |
| final_fix = {"file_path": "a.py", "fixed_content": "print('ok')", "reasoning": "guess"} |
| r1_message = MagicMock() |
| r1_message.tool_calls = None |
| r1_message.content = json.dumps(final_fix) |
|
|
| mock_llm_client = AsyncMock() |
| mock_llm_client.chat.completions.create.return_value = MagicMock(choices=[MagicMock(message=r1_message)]) |
|
|
| mock_ctx = AsyncMock() |
| mock_ctx.__aenter__.return_value = mock_llm_client |
|
|
| with ( |
| patch("src.server.services.dev_ops_agent_service.get_llm_client", return_value=mock_ctx), |
| patch( |
| "src.server.services.dev_ops_agent_service.credential_service.get_active_provider", |
| return_value={"chat_model": "test-model-v1"}, |
| ), |
| ): |
| result = await service.dev_ops._analyze_error_with_structured_output("cmd", "err", agent_id="bcb00484-30bd-46fb-9e39-84b2ec4ced31") |
| assert result == final_fix |
| call_args = mock_llm_client.chat.completions.create.call_args |
| assert call_args.kwargs["tools"] is None |
|
|