Files
hanzoai_agent/sdk/python/tests/test_async_execution_manager_comprehensive.py
zeekay ff7ef7114f feat: merge agents control plane + SDKs into unified agent repo
Consolidate all Hanzo agent infrastructure into one repo:
- control-plane/: Go control plane server (was agents-new)
- sdk/: Go, Python, TypeScript SDKs
- deployments/: Docker, Kubernetes, Helm configs
- Dockerfile.sdk: Python SDK image (ghcr.io/hanzoai/agent:latest)
- CI builds both SDK + control-plane images

No more agentfield — everything is hanzoai/agent.
2026-02-13 12:24:31 -08:00

939 lines
28 KiB
Python

"""
Comprehensive tests for AsyncExecutionManager covering critical execution paths.
"""
import asyncio
import time
from contextlib import asynccontextmanager
from unittest.mock import AsyncMock, MagicMock
import pytest
from hanzo_agents.async_config import AsyncConfig
from hanzo_agents.async_execution_manager import AsyncExecutionManager
from hanzo_agents.execution_state import ExecutionPriority, ExecutionStatus
@pytest.fixture
def mock_connection_manager():
"""Create a mock connection manager."""
manager = MagicMock()
manager.start = AsyncMock()
manager.close = AsyncMock()
manager.get_session = AsyncMock()
return manager
@pytest.fixture
def mock_result_cache():
"""Create a mock result cache."""
cache = MagicMock()
cache.get_execution_result = MagicMock(return_value=None)
cache.set_execution_result = MagicMock()
cache.start = AsyncMock()
cache.stop = AsyncMock()
return cache
@pytest.fixture
def async_config():
"""Create a test async config."""
return AsyncConfig(
initial_poll_interval=0.1,
fast_poll_interval=0.1,
max_active_polls=5,
batch_size=10,
cleanup_interval=1.0,
)
@pytest.fixture
def manager(async_config, mock_connection_manager, mock_result_cache):
"""Create an AsyncExecutionManager instance."""
return AsyncExecutionManager(
base_url="http://test-server",
config=async_config,
connection_manager=mock_connection_manager,
result_cache=mock_result_cache,
)
@pytest.mark.asyncio
async def test_submit_execution_success(manager):
"""Test successful execution submission."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-123",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner",
input_data={"key": "value"},
priority=ExecutionPriority.NORMAL,
)
assert execution_id is not None
assert execution_id in manager._executions
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_submit_execution_with_webhook(manager):
"""Test execution submission with webhook configuration."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-webhook",
"status": "queued",
"webhook_registered": True,
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
webhook_config = {
"url": "https://example.com/webhook",
"secret": "test-secret",
}
execution_id = await manager.submit_execution(
target="agent.reasoner",
input_data={"key": "value"},
webhook=webhook_config,
)
assert execution_id is not None
execution = manager._executions.get(execution_id)
assert execution is not None
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_get_execution_status(manager):
"""Test getting execution status."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-status",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
status = await manager.get_execution_status(execution_id)
assert status is not None
assert status["execution_id"] == execution_id
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_get_execution_status_not_found(manager):
"""Test getting status for non-existent execution."""
status = await manager.get_execution_status("nonexistent-id")
assert status is None
@pytest.mark.asyncio
async def test_poll_execution_status_success(manager):
"""Test successful execution status polling."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-1",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# Polling happens in background via private methods
# We can verify the execution exists and can get its status
status = await manager.get_execution_status(execution_id)
assert status is not None
assert status["execution_id"] == execution_id
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_poll_execution_status_timeout(manager):
"""Test polling timeout handling."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-timeout",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# Polling happens in background, just verify execution exists
status = await manager.get_execution_status(execution_id)
assert status is not None
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_poll_execution_status_error(manager):
"""Test error handling during polling."""
await manager.start()
try:
# Mock error response for submission (post needs to work)
mock_submit_response = MagicMock()
mock_submit_response.status = 200
mock_submit_response.json = AsyncMock(
return_value={
"execution_id": "exec-error",
"status": "queued",
}
)
mock_submit_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_submit_response)
mock_session.get = AsyncMock(side_effect=Exception("Network error"))
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# Polling happens in background, just verify execution exists
status = await manager.get_execution_status(execution_id)
assert status is not None
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_batch_poll_executions(manager):
"""Test batch polling of multiple executions."""
await manager.start()
try:
# Mock connection manager for submissions
call_count = 0
def get_submit_response():
nonlocal call_count
call_count += 1
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": f"exec-{call_count}",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
return mock_response
mock_session = MagicMock()
mock_session.post = AsyncMock(
side_effect=lambda *args, **kwargs: get_submit_response()
)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
# Submit multiple executions
execution_ids = []
for i in range(3):
exec_id = await manager.submit_execution(
target="agent.reasoner", input_data={"index": i}
)
execution_ids.append(exec_id)
# Mock batch poll response
mock_batch_response = MagicMock()
mock_batch_response.status = 200
mock_batch_response.json = AsyncMock(
return_value={
"executions": {
exec_id: {"status": "succeeded", "result": {"index": i}}
for i, exec_id in enumerate(execution_ids)
}
}
)
mock_batch_response.raise_for_status = MagicMock()
mock_session.get = AsyncMock(return_value=mock_batch_response)
# Batch polling is internal, just verify executions exist
for exec_id in execution_ids:
status = await manager.get_execution_status(exec_id)
assert status is not None
assert status["execution_id"] == exec_id
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_get_execution_result_cached(manager):
"""Test retrieving cached execution result."""
await manager.start()
try:
execution_id = "exec-1"
cached_result = {"output": "cached"}
# Set cache
manager.result_cache.get_execution_result = MagicMock(
return_value=cached_result
)
# wait_for_result requires an actual execution, so test cache directly
result = manager.result_cache.get_execution_result(execution_id)
assert result == cached_result
manager.result_cache.get_execution_result.assert_called_once()
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_get_execution_result_not_cached(manager):
"""Test retrieving result when not cached."""
await manager.start()
try:
execution_id = "exec-1"
# No cache
manager.result_cache.get_execution_result = MagicMock(return_value=None)
# get_execution_result doesn't exist, test cache directly
result = manager.result_cache.get_execution_result(execution_id)
assert result is None
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_cleanup_completed_executions(manager):
"""Test cleanup of completed executions."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-cleanup",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
# Submit and mark as completed
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
execution = manager._executions.get(execution_id)
execution.status = ExecutionStatus.SUCCEEDED
# Set metrics end_time to make it old enough for cleanup
execution.metrics.end_time = time.time() - 1000 # Old
# Set retention to a small value so cleanup removes it
manager.config.completed_execution_retention_seconds = 500
await manager.cleanup_completed_executions()
# Execution should be removed
assert execution_id not in manager._executions
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_context_propagation(manager):
"""Test execution context propagation."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-context",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
# Note: submit_execution doesn't have a context parameter
# Context would be passed via headers instead
execution_id = await manager.submit_execution(
target="agent.reasoner",
input_data={"key": "value"},
headers={
"X-Context": '{"parent_id": "parent-123", "session_id": "session-456"}'
},
)
execution = manager._executions.get(execution_id)
assert execution is not None
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_event_stream_subscription(manager):
"""Test event stream subscription."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-stream",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# Mock event stream
events_received = []
async def mock_event_stream():
events_received.append({"type": "status_update", "status": "running"})
events_received.append({"type": "status_update", "status": "succeeded"})
await asyncio.sleep(0.1)
# Start subscription (would normally be handled by background task)
# This is a simplified test
assert execution_id in manager._executions
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_event_stream_reconnection(manager):
"""Test event stream reconnection logic."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-reconnect",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
# This would test the reconnection logic when stream disconnects
# Simplified test for now
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
assert execution_id in manager._executions
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_priority_handling(manager):
"""Test execution priority handling."""
await manager.start()
try:
# Mock connection manager for submissions
mock_response = MagicMock()
mock_response.status = 200
mock_response.raise_for_status = MagicMock()
call_count = 0
def get_response():
nonlocal call_count
call_count += 1
mock_response.json = AsyncMock(
return_value={
"execution_id": f"exec-priority-{call_count}",
"status": "queued",
}
)
return mock_response
mock_session = MagicMock()
mock_session.post = AsyncMock(
side_effect=lambda *args, **kwargs: get_response()
)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
# Submit executions with different priorities
high_priority_id = await manager.submit_execution(
target="agent.reasoner",
input_data={"key": "value"},
priority=ExecutionPriority.HIGH,
)
normal_priority_id = await manager.submit_execution(
target="agent.reasoner",
input_data={"key": "value"},
priority=ExecutionPriority.NORMAL,
)
high_exec = manager._executions.get(high_priority_id)
normal_exec = manager._executions.get(normal_priority_id)
assert high_exec.priority == ExecutionPriority.HIGH
assert normal_exec.priority == ExecutionPriority.NORMAL
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_metrics_tracking(manager):
"""Test metrics tracking."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-metrics",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
def get_metrics(self):
return MagicMock()
manager.connection_manager = DummyConnectionManager(mock_session)
initial_metrics = manager.get_metrics()
# Submit and complete an execution
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
execution = manager._executions.get(execution_id)
execution.status = ExecutionStatus.SUCCEEDED
metrics = manager.get_metrics()
assert metrics["total_executions"] >= initial_metrics["total_executions"] + 1
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_error_handling_during_polling(manager):
"""Test error handling during polling operations."""
await manager.start()
try:
# Mock connection error
class DummyConnectionManager:
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
# Properly yield an exception in context manager
raise Exception("Connection failed")
yield # This will never be reached, but needed for context manager
manager.connection_manager = DummyConnectionManager()
# Submit will fail, but we can test error handling
try:
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# If it succeeds, verify execution exists
status = await manager.get_execution_status(execution_id)
assert status is not None
except Exception:
# Expected to fail due to connection error
pass
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_concurrent_execution_submission(manager):
"""Test concurrent execution submission."""
await manager.start()
try:
# Mock connection manager for submissions
call_count = 0
def get_response():
nonlocal call_count
call_count += 1
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": f"exec-concurrent-{call_count}",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
return mock_response
mock_session = MagicMock()
mock_session.post = AsyncMock(
side_effect=lambda *args, **kwargs: get_response()
)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
async def submit():
return await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# Submit multiple executions concurrently
execution_ids = await asyncio.gather(*[submit() for _ in range(10)])
assert len(execution_ids) == 10
assert all(eid in manager._executions for eid in execution_ids)
finally:
await manager.stop()
@pytest.mark.asyncio
async def test_result_cache_integration(manager):
"""Test result cache integration."""
await manager.start()
try:
# Mock connection manager for submission
mock_response = MagicMock()
mock_response.status = 200
mock_response.json = AsyncMock(
return_value={
"execution_id": "exec-cache",
"status": "queued",
}
)
mock_response.raise_for_status = MagicMock()
mock_session = MagicMock()
mock_session.post = AsyncMock(return_value=mock_response)
class DummyConnectionManager:
def __init__(self, session):
self._session = session
async def start(self):
pass
async def close(self):
pass
@asynccontextmanager
async def get_session(self):
yield self._session
manager.connection_manager = DummyConnectionManager(mock_session)
execution_id = await manager.submit_execution(
target="agent.reasoner", input_data={"key": "value"}
)
# Mock successful execution
execution = manager._executions.get(execution_id)
execution.status = ExecutionStatus.SUCCEEDED
execution.result = {"output": "success"}
# get_execution_result doesn't exist, verify execution has result
status = await manager.get_execution_status(execution_id)
assert status is not None
assert status["status"] == "succeeded"
assert status["result"] == {"output": "success"}
finally:
await manager.stop()