test_tool_execution.py•41.4 kB
"""Unit tests for the TaskExecutor class in tool_execution.py."""
"""MOSTLY AI GENERATED"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
from jentic.lib.agent_runtime.tool_execution import (
TaskExecutor,
)
from jentic.lib.agent_runtime.api_hub import JenticAPIClient
from jentic.lib.models import (
AssociatedFiles,
FileId,
GetFilesResponse,
FileEntry,
OperationEntry,
WorkflowExecutionDetails,
)
from arazzo_runner import (
WorkflowExecutionResult as OakWorkflowExecutionResult,
WorkflowExecutionStatus,
)
@pytest.fixture
def minimal_config():
"""Provide a minimal configuration for testing."""
return {"runtime": {"log_level": "DEBUG"}, "test_api": {"base_url": "https://api.example.com"}}
@pytest.fixture
def comprehensive_config():
"""Provide a more comprehensive configuration for testing."""
return {
"runtime": {"log_level": "INFO", "tool_format": "chatgpt"},
"test_api": {
"base_url": "https://api.example.com",
"auth": {"type": "basic", "username": "test_user", "password": "test_password"},
},
"weather_api": {
"base_url": "https://weather.example.com",
"auth": {"type": "bearer", "token": "test_token"},
},
"email_api": {
"base_url": "https://email.example.com",
"auth": {
"type": "oauth2",
"client_id": "client_123",
"client_secret": "secret_456",
"scopes": ["read", "write"],
},
},
}
@pytest_asyncio.fixture
async def mock_api_hub_client():
"""Create a mocked JenticAPIClient."""
with patch(
"jentic.lib.agent_runtime.tool_execution.JenticAPIClient", autospec=True
) as mock_client_class:
mock_client = AsyncMock(spec=JenticAPIClient)
mock_client_class.return_value = mock_client
yield mock_client
class TestWorkflowExecution:
"""Test suite for workflow execution."""
@pytest.mark.asyncio
async def test_execute_workflow_success(self, mock_api_hub_client):
"""Test successful workflow execution."""
# Prepare test data
workflow_id = "test_workflow_uuid" # External UUID
friendly_workflow_id = "internal_test_workflow" # Internal ID from API
parameters = {"param1": "value1"}
mock_arazzo_doc = {"info": "mock arazzo"}
mock_source_descriptions = {"mock_source": {"openapi": "3.0"}}
# Setup mock API Hub response for the renamed method
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Create a mock runner
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock() # ArazzoRunner.execute_workflow is synchronous
# Update return value structure for ArazzoRunner
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.WORKFLOW_COMPLETE,
workflow_id=friendly_workflow_id,
outputs={"final_output": "success"},
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client) # Pass required api_hub_client
# Call the method
result = await executor.execute_workflow(workflow_id, parameters)
# Verify the calls
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner_class.assert_called_once_with(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
)
# Verify execute_workflow is called with the INTERNAL ID
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs=parameters
)
# Verify the result
assert result.success is True
assert result.output == {"final_output": "success"}
assert result.error is None
assert result.step_results is None # Not populated on success by TaskExecutor
assert result.inputs is None # Not populated on success by TaskExecutor
@pytest.mark.asyncio
async def test_execute_workflow_runner_returns_error(self, mock_api_hub_client):
"""Test workflow execution when the runner returns an error status."""
workflow_id = "test_workflow_runner_error_uuid"
friendly_workflow_id = "internal_test_workflow_runner_error"
parameters = {"param1": "value1"}
mock_arazzo_doc = {"info": "mock arazzo runner error"}
mock_source_descriptions = {"mock_source_runner_error": {}}
expected_step_outputs = {"step1": "failed due to X"}
expected_error_message = "Runner processing failed"
# Setup mock API Hub response
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Create a mock runner that returns an error status
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.ERROR,
workflow_id=friendly_workflow_id,
error=expected_error_message,
outputs=None,
step_outputs=expected_step_outputs,
inputs=parameters,
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method
result = await executor.execute_workflow(workflow_id, parameters)
# Verify the calls
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner_class.assert_called_once_with(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
)
# Verify execute_workflow is called with the INTERNAL ID
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs=parameters
)
# Verify the result
assert result.success is False
assert result.error == expected_error_message
assert result.output is None
assert result.step_results == expected_step_outputs
assert result.inputs == parameters
@pytest.mark.asyncio
async def test_execute_workflow_api_details_error(self, mock_api_hub_client):
"""Test workflow execution when fetching execution details fails."""
workflow_id = "test_workflow_api_fail"
parameters = {"param1": "value1"}
# Setup mock API Hub to return an empty dictionary (details not found)
mock_api_hub_client.get_execution_details_for_workflow.return_value = None
# Patch ArazzoRunner to ensure it's not called
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method
result = await executor.execute_workflow(workflow_id, parameters)
# Assertions
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner_class.assert_not_called()
assert result.success is False
assert f"Execution details not found for workflow {workflow_id}" == result.error
@pytest.mark.asyncio
async def test_execute_workflow_exception(self, mock_api_hub_client):
"""Test workflow execution when an unexpected exception occurs (e.g., runner init)."""
workflow_id = "test_workflow_unexpected_fail"
friendly_workflow_id = "internal_unexpected_fail"
parameters = {"param1": "value1"}
mock_arazzo_doc = {"info": "mock arazzo unexpected"}
mock_source_descriptions = {}
# Setup mock API Hub response for the renamed method
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Mock runner class to raise an exception during instantiation
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner_class.side_effect = Exception("Runner Init Error")
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method
result = await executor.execute_workflow(workflow_id, parameters)
# Verify the calls
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
# Runner execute is not called if init fails
# Verify the result
assert result.success is False
assert "Runner Init Error" in result.error
class TestCompleteWorkflowExecution:
"""Test suite for complete workflow execution scenarios."""
@pytest.mark.asyncio
async def test_complete_execution_with_multiple_steps(self, mock_api_hub_client):
"""Test a complete workflow execution that conceptually involves multiple steps (mocked as one runner call)."""
workflow_id = "multi_step_workflow_uuid"
friendly_workflow_id = "internal_multi_step"
parameters = {"initial_input": "start"}
arazzo_doc = {"info": "multi-step arazzo"}
source_descriptions = {"multi_step_source": {}}
# Setup mock API Hub response
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=arazzo_doc,
source_descriptions=source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Mock the runner
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
# Update return value structure
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.WORKFLOW_COMPLETE,
workflow_id=friendly_workflow_id,
outputs={"final_output": "success"},
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Execute the workflow
result = await executor.execute_workflow(workflow_id, parameters)
# Assertions
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner_class.assert_called_once_with(
arazzo_doc=arazzo_doc,
source_descriptions=source_descriptions,
)
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs=parameters
)
assert result.success is True
assert result.output == {"final_output": "success"}
@pytest.mark.asyncio
async def test_workflow_with_empty_parameters(self, mock_api_hub_client):
"""Test executing a workflow with an empty parameters dictionary."""
workflow_id = "empty_params_workflow_uuid"
friendly_workflow_id = "internal_empty_params"
mock_arazzo_doc = {"some_key": "some_value"}
mock_source_descriptions = {}
# Setup mock API Hub response
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Mock the runner
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
# Update return value structure
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.WORKFLOW_COMPLETE,
workflow_id=friendly_workflow_id,
outputs={"output": "empty_params_success"},
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method with empty parameters
result = await executor.execute_workflow(workflow_id, {})
# Assertions
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs={}
)
assert result.success is True
assert result.output == {"output": "empty_params_success"}
@pytest.mark.asyncio
async def test_workflow_execution_with_no_outputs(self, mock_api_hub_client):
"""Test workflow execution where the runner returns no specific outputs."""
workflow_id = "no_output_workflow_uuid"
friendly_workflow_id = "internal_no_output"
parameters = {"input": "data"}
mock_arazzo_doc = {"some_key": "some_value"}
mock_source_descriptions = {}
# Setup mock API Hub response
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Mock the runner to return an empty dictionary for outputs
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
# Update return value structure
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.WORKFLOW_COMPLETE,
workflow_id=friendly_workflow_id,
outputs={},
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method
result = await executor.execute_workflow(workflow_id, parameters)
# Assertions
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs=parameters
)
assert result.success is True
assert result.output == {}
assert result.error is None
class TestEdgeCases:
"""Tests covering edge cases and specific scenarios for workflow execution."""
@pytest.mark.asyncio
async def test_workflow_with_empty_parameters(self, mock_api_hub_client):
"""Test executing a workflow with an empty parameters dictionary."""
workflow_id = "edge_empty_params_workflow_uuid"
friendly_workflow_id = "internal_edge_empty_params"
mock_arazzo_doc = {"some_key": "some_value"}
mock_source_descriptions = {}
# Setup mock API Hub response
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Mock the runner
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
# Update return value structure
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.WORKFLOW_COMPLETE,
workflow_id=friendly_workflow_id,
outputs={"output": "empty_params_success"},
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method with empty parameters
result = await executor.execute_workflow(workflow_id, {})
# Assertions
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs={}
)
assert result.success is True
assert result.output == {"output": "empty_params_success"}
@pytest.mark.asyncio
async def test_workflow_with_alternate_complete_status(self, mock_api_hub_client):
"""Test workflow execution with an alternate (but valid) completion status."""
workflow_id = "alt_complete_workflow_uuid"
friendly_workflow_id = "internal_alt_complete"
mock_arazzo_doc = {"some_key": "some_value"}
mock_source_descriptions = {}
# Setup mock API Hub response
mock_api_hub_client.get_execution_details_for_workflow.return_value = (
WorkflowExecutionDetails(
arazzo_doc=mock_arazzo_doc,
source_descriptions=mock_source_descriptions,
friendly_workflow_id=friendly_workflow_id,
)
)
# Create mock runner
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
# Runner returns final result directly
# Update return value structure
mock_runner.execute_workflow.return_value = OakWorkflowExecutionResult(
status=WorkflowExecutionStatus.WORKFLOW_COMPLETE,
workflow_id=friendly_workflow_id,
outputs={"result": "success"},
)
mock_runner_class.return_value = mock_runner
# Create the tool executor
executor = TaskExecutor(mock_api_hub_client)
# Call the method
result = await executor.execute_workflow(workflow_id, {})
# Assertions
mock_api_hub_client.get_execution_details_for_workflow.assert_called_once_with(
workflow_id
)
mock_runner.execute_workflow.assert_called_once_with(
workflow_id=friendly_workflow_id, inputs={}
)
assert result.success is True
assert result.output == {"result": "success"}
class TestOperationExecution:
"""Test suite for operation execution."""
@pytest.mark.asyncio
async def test_execute_operation_success(self, mock_api_hub_client):
"""Test successful operation execution with a 200 status and body."""
operation_uuid = "op_success_uuid"
inputs = {"param": "value"}
mock_openapi_content = {"openapi": "3.0.0", "info": {"title": "Test API"}}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="openapi_file_id")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"openapi_file_id": FileEntry(
id="openapi_file_id",
filename="spec.json",
type="open_api",
content=mock_openapi_content,
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
oak_response_body = {"data": "success_data"}
oak_full_response = {"status_code": 200, "body": oak_response_body, "headers": {}}
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
mock_api_hub_client.get_execution_files.assert_called_once_with(
operation_uuids=[operation_uuid]
)
mock_runner_class.assert_called_once_with(
source_descriptions={"default": mock_openapi_content}
)
mock_runner.execute_operation.assert_called_once_with(
inputs=inputs,
operation_path=f"{mock_operation_entry.method} {mock_operation_entry.path}",
)
assert result.success is True
assert result.status_code == 200
assert result.output == oak_response_body
assert result.error is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_success_no_body(self, mock_api_hub_client):
"""Test successful operation execution with 200 status but no 'body' key in OAK result."""
operation_uuid = "op_success_no_body_uuid"
inputs = {}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="POST",
path="/submit",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
oak_full_response = {
"status_code": 204,
"headers": {"X-Custom": "value"},
} # No body for 204
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
assert result.success is True
assert result.status_code == 204
assert result.output == oak_full_response # Should return the full OAK result
assert result.error is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_api_error_4xx(self, mock_api_hub_client):
"""Test operation execution with a 400 client error."""
operation_uuid = "op_client_error_uuid"
inputs = {"bad_param": "invalid"}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
error_detail = {"error": "Bad Request", "message": "Invalid parameter provided"}
oak_full_response = {"status_code": 400, "body": error_detail, "headers": {}}
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
assert result.success is False
assert result.status_code == 400
# The error message is now just the detail from the body
assert result.error == error_detail["error"]
assert result.output == oak_full_response # Full OAK response in output for context
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_api_error_5xx(self, mock_api_hub_client):
"""Test operation execution with a 500 server error."""
operation_uuid = "op_server_error_uuid"
inputs = {}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/status",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
oak_full_response = {"status_code": 503, "body": "Service Unavailable", "headers": {}}
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
assert result.success is False
assert result.status_code == 503
assert result.error == "Service Unavailable"
assert result.output == oak_full_response
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_status_code_as_string(self, mock_api_hub_client):
"""Test successful operation when status_code is a convertible string."""
operation_uuid = "op_status_string_uuid"
inputs = {}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
oak_response_body = {"message": "OK"}
oak_full_response = {"status_code": "201", "body": oak_response_body, "headers": {}}
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
assert result.success is True
assert result.status_code == 201 # Should be cast to int
assert result.output == oak_response_body
assert result.error is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_status_code_invalid_string(self, mock_api_hub_client):
"""Test failure when status_code is a non-convertible string."""
operation_uuid = "op_status_invalid_string_uuid"
inputs = {}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
oak_full_response = {"status_code": "OK_NOT_A_NUMBER", "body": "Error", "headers": {}}
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
assert result.success is False
assert result.status_code is None # Status code is not set on casting failure
assert "Invalid status_code format: 'OK_NOT_A_NUMBER'" in result.error
assert result.output == oak_full_response # Full OAK response in output
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_missing_status_code(self, mock_api_hub_client):
"""Test behavior when ArazzoRunner result is missing 'status_code'."""
operation_uuid = "op_missing_status_uuid"
inputs = {}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
oak_response_body = {"data": "some_data"}
# OAK result missing status_code
oak_full_response = {"body": oak_response_body, "headers": {}}
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_full_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
# Current logic defaults to success if status_code is missing
assert result.success is True
assert result.status_code is None
assert result.output == oak_response_body
assert result.error is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_runner_returns_not_dict(self, mock_api_hub_client):
"""Test behavior when ArazzoRunner returns a non-dictionary result."""
operation_uuid = "op_runner_not_dict_uuid"
inputs = {}
mock_openapi_content = {"openapi": "3.0.0"}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="f1")]),
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"f1": FileEntry(
id="f1", filename="spec.json", type="open_api", content=mock_openapi_content
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
# ArazzoRunner returns a string instead of a dict
oak_non_dict_response = "This is not a dictionary"
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
mock_runner = MagicMock()
mock_runner.execute_operation.return_value = oak_non_dict_response
mock_runner_class.return_value = mock_runner
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
assert result.success is False
assert "object has no attribute 'get'" in result.error
assert result.status_code is None
assert result.output is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_no_operation_entry(self, mock_api_hub_client):
"""Test failure when operation_uuid is not found in API Hub response."""
operation_uuid = "op_not_found_uuid"
inputs = {}
# API Hub returns a response where the operation_uuid is not a key
mock_exec_files_response = GetFilesResponse(workflows={}, operations={}, files={})
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
mock_runner_class.assert_not_called() # ArazzoRunner should not be initialized or called
assert result.success is False
assert (
result.error
== f"Operation ID {operation_uuid} not found in execution files response."
)
assert result.status_code is None
assert result.output is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_no_openapi_spec(self, mock_api_hub_client):
"""Test failure when OpenAPI spec content is missing."""
operation_uuid = "op_no_openapi_uuid"
inputs = {}
# OperationEntry exists, but its files list is empty or points to a non-existent/empty file
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="openapi_file_id")]), # Points to a file
)
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"openapi_file_id": FileEntry(
id="openapi_file_id", filename="spec.json", type="open_api", content={}
)
}
}, # File content is empty
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
mock_runner_class.assert_not_called()
assert result.success is False
assert result.error == f"OpenAPI spec not found for operation {operation_uuid}"
assert result.status_code is None
assert result.output is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_no_openapi_file_entry_in_files(self, mock_api_hub_client):
"""Test failure when OpenAPI spec file ID from operation_entry.files.open_api is not in exec_files_response.files."""
operation_uuid = "op_no_openapi_file_entry_uuid"
inputs = {}
mock_operation_entry = OperationEntry(
api_version_id="v1",
id=operation_uuid,
method="GET",
path="/test",
files=AssociatedFiles(open_api=[FileId(id="actual_openapi_file_id")]),
)
# The 'files' dict does not contain 'actual_openapi_file_id' under 'open_api'
mock_exec_files_response = GetFilesResponse(
workflows={},
operations={operation_uuid: mock_operation_entry},
files={
"open_api": {
"some_other_file_id": FileEntry(
id="some_other_file_id",
filename="spec.json",
type="open_api",
content={"openapi": "3.0"},
)
}
},
)
mock_api_hub_client.get_execution_files.return_value = mock_exec_files_response
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
mock_runner_class.assert_not_called()
assert result.success is False
assert result.error == f"OpenAPI spec not found for operation {operation_uuid}"
assert result.status_code is None
assert result.output is None
assert result.inputs == inputs
@pytest.mark.asyncio
async def test_execute_operation_general_exception_api_call(self, mock_api_hub_client):
"""Test failure when get_execution_files raises an unexpected exception."""
operation_uuid = "op_general_exception_uuid"
inputs = {}
expected_exception = RuntimeError("Network Error")
mock_api_hub_client.get_execution_files.side_effect = expected_exception
with patch("jentic.lib.agent_runtime.tool_execution.ArazzoRunner") as mock_runner_class:
executor = TaskExecutor(api_hub_client=mock_api_hub_client)
result = await executor.execute_operation(operation_uuid, inputs)
mock_runner_class.assert_not_called()
assert result.success is False
assert str(expected_exception) in result.error
assert result.status_code is None
assert result.output is None
assert result.inputs == inputs
if __name__ == "__main__":
pytest.main(["-xvs", __file__])