from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.server.services.agent_service import AgentService @pytest.mark.asyncio # Correctly patch the dependencies at their source, accounting for local imports. @pytest.mark.asyncio # Correctly patch the dependencies at their source, accounting for local imports. @patch("src.server.services.projects.task_service.task_service", new_callable=AsyncMock) @patch("src.server.services.agent_service.get_logger") async def test_run_agent_task_enqueues(mock_get_logger, mock_task_service): """ Unit tests the AgentService.run_agent_task method for default enqueuing. Verifies that it sets status to 'dispatched'. """ # --- Setup --- mock_logger = MagicMock() mock_get_logger.return_value = mock_logger mock_task_service.update_task.return_value = (True, {}) agent_service = AgentService() task_id = "task-123" agent_id = "ai-test-agent" # --- Test Execution --- await agent_service.run_agent_task(task_id, agent_id, immediate=False) # --- Assertions --- # 1. Verify enqueuing logger call mock_logger.info.assert_any_call(f"📥 Enqueuing task '{task_id}' for AI agent '{agent_id}'.") # 2. Verify that task_service.update_task was called with 'dispatched' mock_task_service.update_task.assert_awaited_once_with(task_id, {"status": "dispatched", "assignee": agent_id}) @pytest.mark.asyncio @patch("src.server.services.projects.task_service.task_service", new_callable=AsyncMock) @patch("src.server.services.agent_service.get_logger") async def test_run_agent_task_immediate(mock_get_logger, mock_task_service): """ Tests immediate execution (bypassing queue). """ mock_logger = MagicMock() mock_get_logger.return_value = mock_logger mock_task_service.update_task.return_value = (True, {}) agent_service = AgentService() task_id = "task-123" agent_id = "ai-test-agent" with patch.object(AgentService, "_run_general_agent_task", new_callable=AsyncMock) as mock_general_loop: await agent_service.run_agent_task(task_id, agent_id, immediate=True) mock_general_loop.assert_awaited_once_with(task_id, agent_id) # Assertions mock_logger.info.assert_any_call(f"🚀 AI agent '{agent_id}' starting physical work on task '{task_id}'.") mock_task_service.update_task.assert_awaited_once_with(task_id, {"status": "doing", "assignee": agent_id}) @pytest.mark.asyncio @patch("src.server.services.projects.task_service.task_service", new_callable=AsyncMock) @patch("src.server.services.agent_service.get_logger") async def test_run_agent_task_fails_to_enqueue(mock_get_logger, mock_task_service): """ Tests failure handling when enqueuing. """ # --- Setup --- mock_logger = MagicMock() mock_get_logger.return_value = mock_logger mock_task_service.update_task.return_value = (False, {"error": "DB down"}) agent_service = AgentService() task_id = "task-456" agent_id = "ai-failing-agent" # --- Test Execution --- await agent_service.run_agent_task(task_id, agent_id) # --- Assertions --- mock_logger.info.assert_called_once_with(f"📥 Enqueuing task '{task_id}' for AI agent '{agent_id}'.") mock_logger.error.assert_called_once_with("Failed to enqueue task: DB down") @pytest.mark.asyncio @patch("src.server.services.dev_ops_agent_service.asyncio.create_subprocess_shell") @patch("src.server.services.dev_ops_agent_service.get_logger") async def test_run_command_success(mock_get_logger, mock_subprocess): """ Test run_command_with_self_healing when command succeeds. """ mock_logger = MagicMock() mock_get_logger.return_value = mock_logger # Mock subprocess success mock_process = AsyncMock() mock_process.communicate.return_value = (b"success output", b"") mock_process.returncode = 0 mock_subprocess.return_value = mock_process agent_service = AgentService() success, output = await agent_service.dev_ops.run_command_with_self_healing("echo test", agent_id="test-bot") assert success is True assert output == "success output" mock_logger.info.assert_called_with("Command 'echo test' succeeded initially.") @pytest.mark.asyncio @patch("src.server.services.dev_ops_agent_service.credential_service") @patch("src.server.services.dev_ops_agent_service.get_llm_client") @patch("src.server.services.dev_ops_agent_service.asyncio.create_subprocess_shell") @patch("src.server.services.dev_ops_agent_service.get_logger") async def test_run_command_failure_triggers_healing( mock_get_logger, mock_subprocess, mock_get_llm, mock_credential_service ): """ Test run_command_with_self_healing when command fails. Verifies that it calls the LLM for analysis. """ mock_logger = MagicMock() mock_get_logger.return_value = mock_logger # Mock subprocess failure mock_process = AsyncMock() mock_process.communicate.return_value = (b"", b"Error: command failed") mock_process.returncode = 1 mock_subprocess.return_value = mock_process # Mock Credential Service mock_credential_service.get_active_provider = AsyncMock(return_value={"chat_model": "gpt-4-test"}) # Need to mock get_credential as it is awaited in the service mock_credential_service.get_credential = AsyncMock(return_value="test-api-key") # Mock LLM Client mock_client = AsyncMock() mock_get_llm.return_value.__aenter__.return_value = mock_client mock_response = MagicMock() # Return valid JSON but without file_path to trigger the "Analysis only" path (line 108 in service) mock_response.choices = [MagicMock(message=MagicMock(content='{"reasoning": "Check syntax"}'))] mock_client.chat.completions.create.return_value = mock_response agent_service = AgentService() success, output = await agent_service.dev_ops.run_command_with_self_healing("failing_cmd", agent_id="test-bot") assert success is False # Verify security block (Agent has 0 successes in test environment) assert "Poisson Security Block" in output assert "Proposal: Check syntax" in output # Verify LLM was called mock_client.chat.completions.create.assert_awaited_once() # Verify logger warned about failure mock_logger.warning.assert_any_call("Command 'failing_cmd' failed. Starting Active Repair Loop.") mock_logger.warning.assert_any_call( "Poisson Gate: Insufficient credibility for Level 2 auto-repair. (Unknown (XP 0))" )