mirror of
https://github.com/hanzoai/agent.git
synced 2026-09-11 21:08:51 +00:00
Every path in this suite registers /v1/... and none registers /api/v1/,
while the client built its base as {url}/api/v1 — so twelve client tests
failed the moment collection worked again. The extra segment is not a
shape this estate serves and nothing asserted it.
The UI subtree was built by replacing the first "/v1" found, under a
comment claiming it replaced the last. Appended instead, so a base url
carrying its own version segment cannot be rewritten. The test stub kept
a second copy of the client's base and had drifted from it.
Co-authored-by: Hanzo Dev <[email protected]>
436 lines
12 KiB
Python
436 lines
12 KiB
Python
"""
|
|
Comprehensive tests for HanzoAgentsClient execution paths.
|
|
"""
|
|
|
|
import asyncio
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
import responses as responses_lib
|
|
|
|
from hanzo_agents.client import HanzoAgentsClient
|
|
from hanzo_agents.execution_context import ExecutionContext
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create a test client."""
|
|
return HanzoAgentsClient(base_url="http://localhost:8080")
|
|
|
|
|
|
def test_call_sync_execution(client):
|
|
"""Test synchronous execution call."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-123"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-123",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response is not None
|
|
|
|
|
|
def test_call_async_execution(client):
|
|
"""Test asynchronous execution call."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-456"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-456",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response is not None
|
|
assert "status" in response or "result" in response
|
|
|
|
|
|
def test_call_with_context_headers(client):
|
|
"""Test call with execution context headers."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-789"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-789",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
mock_agent = MagicMock()
|
|
context = ExecutionContext(
|
|
execution_id="exec-1",
|
|
run_id="run-1",
|
|
agent_instance=mock_agent,
|
|
reasoner_name="reasoner-1",
|
|
parent_execution_id="parent-1",
|
|
)
|
|
|
|
# Set the context on the client
|
|
client._current_workflow_context = context
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response is not None
|
|
|
|
|
|
def test_call_error_handling(client):
|
|
"""Test error handling in call method."""
|
|
# Mock error response from the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"error": "Network error"},
|
|
status=500,
|
|
)
|
|
|
|
with pytest.raises(Exception):
|
|
client.execute_sync(target="agent.reasoner", input_data={"key": "value"})
|
|
|
|
|
|
def test_call_retry_logic(client):
|
|
"""Test retry logic for failed requests."""
|
|
# First call fails with 500
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"error": "Transient error"},
|
|
status=500,
|
|
)
|
|
# Second call succeeds
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-retry"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-retry",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
# Should retry and eventually succeed
|
|
# Note: This test may fail if the client doesn't implement retry logic
|
|
# In that case, it will raise an exception on the first 500 response
|
|
try:
|
|
response = client.execute_sync(
|
|
target="agent.reasoner", input_data={"key": "value"}
|
|
)
|
|
assert response is not None
|
|
except Exception:
|
|
# If client doesn't retry, this is expected behavior
|
|
pass
|
|
|
|
|
|
def test_call_with_webhook_config(client):
|
|
"""Test call with webhook configuration."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-webhook"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-webhook",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
webhook_config = {
|
|
"url": "https://example.com/webhook",
|
|
"secret": "test-secret",
|
|
}
|
|
|
|
# Note: execute_sync doesn't accept webhook parameter directly
|
|
# Webhook config would be passed via input_data
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value", "webhook": webhook_config},
|
|
)
|
|
|
|
assert response is not None
|
|
|
|
|
|
def test_call_header_propagation(client):
|
|
"""Test header propagation in call method."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-header"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-header",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
# Set current workflow context
|
|
mock_agent = MagicMock()
|
|
context = ExecutionContext(
|
|
execution_id="exec-1",
|
|
run_id="run-1",
|
|
agent_instance=mock_agent,
|
|
reasoner_name="reasoner-1",
|
|
)
|
|
client._current_workflow_context = context
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response is not None
|
|
|
|
|
|
def test_call_event_stream_handling(client):
|
|
"""Test event stream handling in call method."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-stream"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint with streaming response
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-stream",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response is not None
|
|
|
|
|
|
def test_call_timeout_handling(client):
|
|
"""Test timeout handling in call method."""
|
|
# Mock timeout error using responses
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
body=asyncio.TimeoutError(),
|
|
)
|
|
|
|
with pytest.raises((asyncio.TimeoutError, Exception)):
|
|
client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
|
|
def test_call_with_different_execution_modes(client):
|
|
"""Test call with different execution modes."""
|
|
# Mock first call
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-mode1"},
|
|
status=200,
|
|
)
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-mode1",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
# Mock second call
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-mode2"},
|
|
status=200,
|
|
)
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-mode2",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
# Test sync mode
|
|
response_sync = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
assert response_sync is not None
|
|
|
|
# Test async mode (both use execute_sync in current implementation)
|
|
response_async = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
assert response_async is not None
|
|
|
|
|
|
def test_call_result_caching(client):
|
|
"""Test result caching in call method."""
|
|
# Mock first call
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-cache1"},
|
|
status=200,
|
|
)
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-cache1",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
# Mock second call
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-cache2"},
|
|
status=200,
|
|
)
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-cache2",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
# First call
|
|
response1 = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
# Second call with same input (should use cache if enabled)
|
|
response2 = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response1 is not None
|
|
assert response2 is not None
|
|
|
|
|
|
def test_call_with_custom_headers(client):
|
|
"""Test call with custom headers."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-custom"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-custom",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
custom_headers = {"X-Custom-Header": "custom-value"}
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
headers=custom_headers,
|
|
)
|
|
|
|
assert response is not None
|
|
|
|
|
|
def test_call_context_management(client):
|
|
"""Test context management in call method."""
|
|
# Mock the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"execution_id": "exec-context"},
|
|
status=200,
|
|
)
|
|
# Mock the status polling endpoint
|
|
responses_lib.add(
|
|
responses_lib.GET,
|
|
"http://localhost:8080/v1/executions/exec-context",
|
|
json={"status": "succeeded", "result": {"key": "value"}},
|
|
status=200,
|
|
)
|
|
|
|
# Test that context is properly managed
|
|
mock_agent = MagicMock()
|
|
context = ExecutionContext(
|
|
execution_id="exec-1",
|
|
run_id="run-1",
|
|
agent_instance=mock_agent,
|
|
reasoner_name="reasoner-1",
|
|
)
|
|
|
|
# Set context
|
|
client._current_workflow_context = context
|
|
|
|
response = client.execute_sync(
|
|
target="agent.reasoner",
|
|
input_data={"key": "value"},
|
|
)
|
|
|
|
assert response is not None
|
|
# Context should still be set
|
|
assert client._current_workflow_context == context
|
|
|
|
|
|
def test_call_error_response_handling(client):
|
|
"""Test handling of error responses."""
|
|
# Mock error response from the async execution endpoint
|
|
responses_lib.add(
|
|
responses_lib.POST,
|
|
"http://localhost:8080/v1/execute/async/agent.reasoner",
|
|
json={"error": "Internal server error"},
|
|
status=500,
|
|
)
|
|
|
|
with pytest.raises(Exception):
|
|
client.execute_sync(target="agent.reasoner", input_data={"key": "value"})
|