myrmidon / python /tests /server /services /test_agent_service.py
tek Atrust
chore(deploy): build monolithic server for Hugging Face
d5ef46f
Raw
History Blame Contribute Delete
6.52 kB
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))"
)