Python: DevUI, Fix message serialization issue, improve tests (#2674)

* fix message serialization issue, improve tests for devui

* update tests
This commit is contained in:
Victor Dibia
2025-12-05 18:20:54 -08:00
committed by GitHub
Unverified
parent 2d3ba95036
commit 8c6b12e664
10 changed files with 1431 additions and 373 deletions
+434 -276
View File
@@ -1,18 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
"""Clean focused tests for message mapping functionality."""
"""Tests for message mapping functionality.
This module tests the MessageMapper which converts Agent Framework events
to OpenAI-compatible streaming events. Tests use REAL classes from
agent_framework, not mocks, to ensure proper serialization.
"""
import asyncio
import sys
from pathlib import Path
from typing import Any
import pytest
# Add the main agent_framework package for real types
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "main"))
# Import Agent Framework types (assuming they are always available)
# Import Agent Framework types
from agent_framework._types import (
AgentRunResponseUpdate,
ErrorContent,
@@ -22,8 +21,51 @@ from agent_framework._types import (
TextContent,
)
# Import real workflow event classes - NOT mocks!
from agent_framework._workflows._events import (
ExecutorCompletedEvent,
WorkflowStartedEvent,
WorkflowStatusEvent,
)
# Import test utilities
from test_helpers import (
create_agent_run_response,
create_executor_completed_event,
create_executor_failed_event,
create_executor_invoked_event,
create_mapper,
create_test_request,
)
from agent_framework_devui._mapper import MessageMapper
from agent_framework_devui.models._openai_custom import AgentFrameworkRequest
from agent_framework_devui.models._openai_custom import (
AgentCompletedEvent,
AgentFailedEvent,
AgentFrameworkRequest,
AgentStartedEvent,
)
# =============================================================================
# Local Fixtures (to replace conftest.py fixtures)
# =============================================================================
@pytest.fixture
def mapper() -> MessageMapper:
"""Create a fresh MessageMapper for each test."""
return create_mapper()
@pytest.fixture
def test_request() -> AgentFrameworkRequest:
"""Create a standard test request."""
return create_test_request()
# =============================================================================
# Test Helpers
# =============================================================================
def create_test_content(content_type: str, **kwargs: Any) -> Any:
@@ -41,37 +83,26 @@ def create_test_content(content_type: str, **kwargs: Any) -> Any:
raise ValueError(f"Unknown content type: {content_type}")
def create_test_agent_update(contents: list[Any]) -> Any:
"""Create test AgentRunResponseUpdate - NO fake attributes!"""
def create_test_agent_update(contents: list[Any]) -> AgentRunResponseUpdate:
"""Create test AgentRunResponseUpdate."""
return AgentRunResponseUpdate(
contents=contents, role=Role.ASSISTANT, message_id="test_msg", response_id="test_resp"
)
@pytest.fixture
def mapper() -> MessageMapper:
return MessageMapper()
@pytest.fixture
def test_request() -> AgentFrameworkRequest:
# Use metadata.entity_id for routing
return AgentFrameworkRequest(
metadata={"entity_id": "test_agent"},
input="Test input",
stream=True,
)
# =============================================================================
# Basic Content Mapping Tests
# =============================================================================
async def test_critical_isinstance_bug_detection(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""CRITICAL: Test that would have caught the isinstance vs hasattr bug."""
content = create_test_content("text", text="Bug detection test")
update = create_test_agent_update([content])
# Key assertions that would have caught the bug
assert hasattr(update, "contents") # Real attribute
assert not hasattr(update, "response") # Fake attribute should not exist
assert hasattr(update, "contents") # Real attribute
assert not hasattr(update, "response") # Fake attribute should not exist
# Test isinstance works with real types
assert isinstance(update, AgentRunResponseUpdate)
@@ -81,7 +112,6 @@ async def test_critical_isinstance_bug_detection(mapper: MessageMapper, test_req
assert len(events) > 0
assert all(hasattr(event, "type") for event in events)
# Should never get unknown events with proper types
assert all(event.type != "unknown" for event in events)
@@ -136,13 +166,12 @@ async def test_function_result_content_with_string_result(
"""Test FunctionResultContent with plain string result (regular tools)."""
content = FunctionResultContent(
call_id="test_call_123",
result="Hello, World!", # Plain string like regular Python function tools
result="Hello, World!",
)
update = create_test_agent_update([content])
events = await mapper.convert_event(update, test_request)
# Should produce response.function_result.complete event
assert len(events) >= 1
result_events = [e for e in events if e.type == "response.function_result.complete"]
assert len(result_events) == 1
@@ -154,59 +183,22 @@ async def test_function_result_content_with_string_result(
async def test_function_result_content_with_nested_content_objects(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test FunctionResultContent with nested Content objects (MCP tools case).
This tests the issue from GitHub #1476 where MCP tools return FunctionResultContent
with nested TextContent objects that fail to serialize properly.
"""
# This is what MCP tools return - result contains nested Content objects
"""Test FunctionResultContent with nested Content objects (MCP tools case)."""
content = FunctionResultContent(
call_id="mcp_call_456",
result=[TextContent(text="Hello from MCP!")], # List containing TextContent object
result=[TextContent(text="Hello from MCP!")],
)
update = create_test_agent_update([content])
events = await mapper.convert_event(update, test_request)
# Should successfully serialize the nested Content object
assert len(events) >= 1
result_events = [e for e in events if e.type == "response.function_result.complete"]
assert len(result_events) == 1
# The output should contain the text from the nested TextContent
# Should not have TypeError or empty output
assert result_events[0].output != ""
assert "Hello from MCP!" in result_events[0].output
assert result_events[0].call_id == "mcp_call_456"
async def test_function_result_content_with_multiple_nested_content_objects(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test FunctionResultContent with multiple nested Content objects."""
# MCP tools can return multiple Content objects
content = FunctionResultContent(
call_id="mcp_call_789",
result=[
TextContent(text="First result"),
TextContent(text="Second result"),
],
)
update = create_test_agent_update([content])
events = await mapper.convert_event(update, test_request)
assert len(events) >= 1
result_events = [e for e in events if e.type == "response.function_result.complete"]
assert len(result_events) == 1
# Should serialize all nested Content objects
output = result_events[0].output
assert output != ""
assert "First result" in output
assert "Second result" in output
async def test_error_content_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test ErrorContent mapping."""
content = create_test_content("error", message="Test error", code="test_code")
@@ -232,75 +224,32 @@ async def test_mixed_content_types(mapper: MessageMapper, test_request: AgentFra
events = await mapper.convert_event(update, test_request)
assert len(events) >= 3
# Should have both types of events
event_types = {event.type for event in events}
assert "response.output_text.delta" in event_types
assert "response.function_call_arguments.delta" in event_types
async def test_unknown_content_fallback(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test graceful handling of unknown content types."""
# Test the fallback path directly since we can't create invalid AgentRunResponseUpdate
# due to Pydantic validation. Instead, test the content mapper's unknown content handling.
class MockUnknownContent:
def __init__(self):
self.__class__.__name__ = "WeirdUnknownContent" # Not in content_mappers
# Test the content mapper directly
context = mapper._get_or_create_context(test_request)
unknown_content = MockUnknownContent()
# This should trigger the unknown content fallback in _convert_agent_update
event = await mapper._create_unknown_content_event(unknown_content, context)
assert event.type == "response.output_text.delta"
assert "Unknown content type" in event.delta
assert "WeirdUnknownContent" in event.delta
async def test_agent_run_response_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test that mapper handles complete AgentRunResponse (non-streaming)."""
from agent_framework import AgentRunResponse, ChatMessage, Role, TextContent
# Create a complete response like agent.run() would return
message = ChatMessage(
role=Role.ASSISTANT,
contents=[TextContent(text="Complete response from run()")],
)
response = AgentRunResponse(messages=[message], response_id="test_resp_123")
# Mapper should convert it to streaming events
events = await mapper.convert_event(response, test_request)
assert len(events) > 0
# Should produce text delta events
text_events = [e for e in events if e.type == "response.output_text.delta"]
assert len(text_events) > 0
assert text_events[0].delta == "Complete response from run()"
# =============================================================================
# Agent Lifecycle Event Tests
# =============================================================================
async def test_agent_lifecycle_events(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test that agent lifecycle events are properly converted to OpenAI format."""
from agent_framework_devui.models._openai_custom import AgentCompletedEvent, AgentFailedEvent, AgentStartedEvent
# Test AgentStartedEvent
start_event = AgentStartedEvent()
events = await mapper.convert_event(start_event, test_request)
assert len(events) == 2 # Should emit response.created and response.in_progress
assert len(events) == 2 # response.created and response.in_progress
assert events[0].type == "response.created"
assert events[1].type == "response.in_progress"
assert events[0].response.model == "devui" # Should use 'devui' when model not specified in request
assert events[0].response.model == "devui"
assert events[0].response.status == "in_progress"
# Test AgentCompletedEvent
complete_event = AgentCompletedEvent()
events = await mapper.convert_event(complete_event, test_request)
# AgentCompletedEvent no longer emits response.completed to avoid duplicates
# The server will emit the final response.completed with usage data
assert len(events) == 0
# Test AgentFailedEvent
@@ -312,154 +261,218 @@ async def test_agent_lifecycle_events(mapper: MessageMapper, test_request: Agent
assert events[0].type == "response.failed"
assert events[0].response.status == "failed"
assert events[0].response.error.message == "Test error"
assert events[0].response.error.code == "server_error"
@pytest.mark.skip(reason="Workflow events need real classes from agent_framework.workflows")
async def test_workflow_lifecycle_events(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test that workflow lifecycle events are properly converted to OpenAI format."""
async def test_agent_run_response_mapping(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test that mapper handles complete AgentRunResponse (non-streaming)."""
response = create_agent_run_response("Complete response from run()")
# Create mock workflow events (since we don't have access to the real ones in tests)
class WorkflowStartedEvent: # noqa: B903
def __init__(self, workflow_id: str):
self.workflow_id = workflow_id
events = await mapper.convert_event(response, test_request)
class WorkflowCompletedEvent: # noqa: B903
def __init__(self, workflow_id: str):
self.workflow_id = workflow_id
class WorkflowFailedEvent: # noqa: B903
def __init__(self, workflow_id: str, error_info: dict | None = None):
self.workflow_id = workflow_id
self.error_info = error_info
# Test WorkflowStartedEvent
start_event = WorkflowStartedEvent(workflow_id="test_workflow_123")
events = await mapper.convert_event(start_event, test_request)
assert len(events) == 2 # Should emit response.created and response.in_progress
assert events[0].type == "response.created"
assert events[1].type == "response.in_progress"
assert events[0].response.model == "test_agent" # Should use model from request
assert events[0].response.status == "in_progress"
# Test WorkflowCompletedEvent
complete_event = WorkflowCompletedEvent(workflow_id="test_workflow_123")
events = await mapper.convert_event(complete_event, test_request)
# WorkflowCompletedEvent no longer emits response.completed to avoid duplicates
# The server will emit the final response.completed with usage data
assert len(events) == 0
# Test WorkflowFailedEvent with error info
failed_event = WorkflowFailedEvent(workflow_id="test_workflow_123", error_info={"message": "Workflow failed"})
events = await mapper.convert_event(failed_event, test_request)
assert len(events) == 1
assert events[0].type == "response.failed"
assert events[0].response.status == "failed"
assert events[0].response.error.message == "{'message': 'Workflow failed'}"
assert events[0].response.error.code == "server_error"
assert len(events) > 0
text_events = [e for e in events if e.type == "response.output_text.delta"]
assert len(text_events) > 0
assert text_events[0].delta == "Complete response from run()"
@pytest.mark.skip(reason="Executor events need real classes from agent_framework.workflows")
async def test_executor_action_events(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test that workflow executor events are properly converted to custom output item events."""
# =============================================================================
# Workflow Executor Event Tests (using REAL classes, not mocks!)
# =============================================================================
# Create mock executor events (since we don't have access to the real ones in tests)
class ExecutorInvokedEvent: # noqa: B903
def __init__(self, executor_id: str, executor_type: str = "test"):
self.executor_id = executor_id
self.executor_type = executor_type
class ExecutorCompletedEvent: # noqa: B903
def __init__(self, executor_id: str, result: Any = None):
self.executor_id = executor_id
self.result = result
async def test_executor_invoked_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test ExecutorInvokedEvent using the REAL class from agent_framework."""
# Use real class, not mock!
event = create_executor_invoked_event(executor_id="exec_123")
class ExecutorFailedEvent: # noqa: B903
def __init__(self, executor_id: str, error: Exception | None = None):
self.executor_id = executor_id
self.error = error
# Test ExecutorInvokedEvent
invoked_event = ExecutorInvokedEvent(executor_id="exec_123", executor_type="test_executor")
events = await mapper.convert_event(invoked_event, test_request)
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
assert events[0].type == "response.output_item.added"
assert events[0].item["type"] == "executor_action"
assert events[0].item["executor_id"] == "exec_123"
assert events[0].item["status"] == "in_progress"
# Access as dict since item might be ExecutorActionItem
item = events[0].item if isinstance(events[0].item, dict) else events[0].item.model_dump()
assert item["type"] == "executor_action"
assert item["executor_id"] == "exec_123"
assert item["status"] == "in_progress"
# Test ExecutorCompletedEvent
complete_event = ExecutorCompletedEvent(executor_id="exec_123", result={"data": "success"})
events = await mapper.convert_event(complete_event, test_request)
async def test_executor_completed_event_simple_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test ExecutorCompletedEvent with simple dict data."""
# Create event with simple data
event = ExecutorCompletedEvent(executor_id="exec_123", data={"simple": "result"})
# First need to invoke the executor to set up context
invoke_event = create_executor_invoked_event(executor_id="exec_123")
await mapper.convert_event(invoke_event, test_request)
# Now complete it
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
assert events[0].type == "response.output_item.done"
assert events[0].item["type"] == "executor_action"
assert events[0].item["executor_id"] == "exec_123"
assert events[0].item["status"] == "completed"
assert events[0].item["result"] == {"data": "success"}
item = events[0].item if isinstance(events[0].item, dict) else events[0].item.model_dump()
assert item["type"] == "executor_action"
assert item["executor_id"] == "exec_123"
assert item["status"] == "completed"
# Result should be serialized
assert item["result"] == {"simple": "result"}
# Test ExecutorFailedEvent
failed_event = ExecutorFailedEvent(executor_id="exec_123", error=Exception("Executor failed"))
events = await mapper.convert_event(failed_event, test_request)
async def test_executor_completed_event_with_agent_response(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test ExecutorCompletedEvent with nested AgentExecutorResponse.
This is a REGRESSION TEST for the serialization bug where
ExecutorCompletedEvent.data contained AgentExecutorResponse with nested
AgentRunResponse and ChatMessage objects (SerializationMixin) that
Pydantic couldn't serialize.
"""
# Create event with realistic nested data - the exact structure that caused the bug
event = create_executor_completed_event(executor_id="exec_agent", with_agent_response=True)
# Verify the data has the problematic structure
assert hasattr(event.data, "agent_run_response")
assert hasattr(event.data, "full_conversation")
# First invoke the executor
invoke_event = create_executor_invoked_event(executor_id="exec_agent")
await mapper.convert_event(invoke_event, test_request)
# Now complete - this should NOT raise serialization errors
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
assert events[0].type == "response.output_item.done"
assert events[0].item["type"] == "executor_action"
assert events[0].item["executor_id"] == "exec_123"
assert events[0].item["status"] == "failed"
assert "Executor failed" in str(events[0].item["error"]["message"])
# Get the item (might be Pydantic model or dict)
item = events[0].item if isinstance(events[0].item, dict) else events[0].item.model_dump()
assert item["type"] == "executor_action"
assert item["executor_id"] == "exec_agent"
assert item["status"] == "completed"
# The result should be serialized (converted to dict)
result = item["result"]
assert result is not None
# Should be a dict or list, not the original object
assert isinstance(result, (dict, list))
async def test_executor_completed_event_serialization_to_json(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""REGRESSION TEST: Verify the full JSON serialization works.
This tests the exact failure mode from the bug: calling model_dump_json()
on the event containing nested SerializationMixin objects.
"""
# Create the problematic event
event = create_executor_completed_event(executor_id="exec_json_test", with_agent_response=True)
# Invoke first
invoke_event = create_executor_invoked_event(executor_id="exec_json_test")
await mapper.convert_event(invoke_event, test_request)
# Complete
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
done_event = events[0]
# This is the critical test - model_dump_json() should NOT raise
# "Unable to serialize unknown type: <class 'agent_framework._types.AgentRunResponse'>"
try:
json_str = done_event.model_dump_json()
assert json_str is not None
assert len(json_str) > 0
# Verify it's valid JSON by checking it contains expected fields
assert "executor_action" in json_str
assert "exec_json_test" in json_str
assert "completed" in json_str
except Exception as e:
pytest.fail(f"model_dump_json() raised an exception: {e}")
async def test_executor_failed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test ExecutorFailedEvent using the REAL class."""
# First invoke the executor
invoke_event = create_executor_invoked_event(executor_id="exec_fail")
await mapper.convert_event(invoke_event, test_request)
# Now fail it
event = create_executor_failed_event(executor_id="exec_fail", error_message="Executor failed")
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
assert events[0].type == "response.output_item.done"
item = events[0].item if isinstance(events[0].item, dict) else events[0].item.model_dump()
assert item["type"] == "executor_action"
assert item["executor_id"] == "exec_fail"
assert item["status"] == "failed"
assert "Executor failed" in str(item["error"])
# =============================================================================
# Workflow Lifecycle Event Tests
# =============================================================================
async def test_workflow_started_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowStartedEvent using the REAL class."""
event = WorkflowStartedEvent(data=None)
events = await mapper.convert_event(event, test_request)
# WorkflowStartedEvent should emit response.created and response.in_progress
assert len(events) == 2
assert events[0].type == "response.created"
assert events[1].type == "response.in_progress"
async def test_workflow_status_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowStatusEvent using the REAL class."""
from agent_framework._workflows._events import WorkflowRunState
event = WorkflowStatusEvent(state=WorkflowRunState.IN_PROGRESS)
events = await mapper.convert_event(event, test_request)
# Should emit some status-related event
assert len(events) >= 0 # May emit events or may be filtered
# =============================================================================
# MagenticAgentDeltaEvent Tests
# =============================================================================
async def test_magentic_agent_delta_creates_message_container(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test that MagenticAgentDeltaEvent creates message containers when no executor context (fallback)."""
# Create mock MagenticAgentDeltaEvent that mimics the real class
"""Test that MagenticAgentDeltaEvent creates message containers when no executor context."""
from dataclasses import dataclass
try:
from agent_framework import WorkflowEvent
from agent_framework import WorkflowEvent
@dataclass
class MagenticAgentDeltaEvent(WorkflowEvent): # Inherit from WorkflowEvent
agent_id: str
text: str | None = None
@dataclass
class MagenticAgentDeltaEvent(WorkflowEvent):
agent_id: str
text: str | None = None
except ImportError:
# Fallback if WorkflowEvent is not available
@dataclass
class MagenticAgentDeltaEvent: # Use the expected name directly
agent_id: str
text: str | None = None
# First delta should create message container (no executor context = fallback behavior)
# First delta should create message container
first_delta = MagenticAgentDeltaEvent(agent_id="test_agent", text="Hello ")
events = await mapper.convert_event(first_delta, test_request)
# Should emit 3 events: message container, content part, and text delta
assert len(events) == 3
assert events[0].type == "response.output_item.added"
assert events[0].item.type == "message" # Message, not executor_action!
assert events[0].item.type == "message"
assert events[0].item.metadata["agent_id"] == "test_agent"
assert events[0].item.metadata["source"] == "magentic"
message_id = events[0].item.id
# Check text delta references the message ID
assert events[2].type == "response.output_text.delta"
assert events[2].item_id == message_id
assert events[2].delta == "Hello "
# Second delta should NOT create new container
second_delta = MagenticAgentDeltaEvent(agent_id="test_agent", text="world!")
events = await mapper.convert_event(second_delta, test_request)
# Only text delta, no new container
assert len(events) == 1
assert events[0].type == "response.output_text.delta"
assert events[0].item_id == message_id
@@ -469,78 +482,223 @@ async def test_magentic_agent_delta_routes_to_executor_item(
mapper: MessageMapper, test_request: AgentFrameworkRequest
) -> None:
"""Test that MagenticAgentDeltaEvent routes to executor item when executor context is present."""
from dataclasses import dataclass
try:
from agent_framework import WorkflowEvent
from agent_framework import WorkflowEvent
@dataclass
class ExecutorInvokedEvent(WorkflowEvent):
executor_id: str
@dataclass
class MagenticAgentDeltaEvent(WorkflowEvent):
agent_id: str
text: str | None = None
@dataclass
class MagenticAgentDeltaEvent(WorkflowEvent):
agent_id: str
text: str | None = None
except ImportError:
@dataclass
class ExecutorInvokedEvent:
executor_id: str
@dataclass
class MagenticAgentDeltaEvent:
agent_id: str
text: str | None = None
# First, simulate executor being invoked (sets current_executor_id in context)
executor_event = ExecutorInvokedEvent(executor_id="agent_writer")
# First, invoke an executor (sets current_executor_id in context)
executor_event = create_executor_invoked_event(executor_id="agent_writer")
executor_events = await mapper.convert_event(executor_event, test_request)
# Should create executor item
assert len(executor_events) == 1
assert executor_events[0].type == "response.output_item.added"
assert executor_events[0].item.type == "executor_action"
executor_item_id = executor_events[0].item.id
# Now send Magentic delta - should route to executor's item, NOT create new message
# Now send Magentic delta - should route to executor's item
delta = MagenticAgentDeltaEvent(agent_id="writer", text="Hello world")
delta_events = await mapper.convert_event(delta, test_request)
# Should only emit 1 event: text delta routed to executor's item
assert len(delta_events) == 1
assert delta_events[0].type == "response.output_text.delta"
assert delta_events[0].item_id == executor_item_id # Routed to executor's item!
assert delta_events[0].item_id == executor_item_id
assert delta_events[0].delta == "Hello world"
if __name__ == "__main__":
# Simple test runner
async def run_all_tests() -> None:
mapper = MessageMapper()
test_request = AgentFrameworkRequest(
metadata={"entity_id": "test"},
input="Test",
stream=True,
)
# =============================================================================
# Unknown Content Fallback Tests
# =============================================================================
tests = [
("Critical isinstance bug detection", test_critical_isinstance_bug_detection),
("Text content mapping", test_text_content_mapping),
("Function call mapping", test_function_call_mapping),
("Error content mapping", test_error_content_mapping),
("Mixed content types", test_mixed_content_types),
("Unknown content fallback", test_unknown_content_fallback),
]
passed = 0
for _test_name, test_func in tests:
try:
await test_func(mapper, test_request)
passed += 1
except Exception:
pass
async def test_unknown_content_fallback(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test graceful handling of unknown content types."""
asyncio.run(run_all_tests())
class MockUnknownContent:
def __init__(self):
self.__class__.__name__ = "WeirdUnknownContent"
context = mapper._get_or_create_context(test_request)
unknown_content = MockUnknownContent()
event = await mapper._create_unknown_content_event(unknown_content, context)
assert event.type == "response.output_text.delta"
assert "Unknown content type" in event.delta
assert "WeirdUnknownContent" in event.delta
# =============================================================================
# WorkflowOutputEvent Tests
# =============================================================================
async def test_workflow_output_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowOutputEvent is converted to output_item.added."""
from agent_framework._workflows._events import WorkflowOutputEvent
event = WorkflowOutputEvent(data="Final workflow output", source_executor_id="final_executor")
events = await mapper.convert_event(event, test_request)
# WorkflowOutputEvent should emit output_item.added
assert len(events) == 1
assert events[0].type == "response.output_item.added"
# Check item contains the output text
item = events[0].item
assert item.type == "message"
assert any("Final workflow output" in str(c) for c in item.content)
async def test_workflow_output_event_with_list_data(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowOutputEvent with list data (common for sequential/concurrent workflows)."""
from agent_framework import ChatMessage, Role, TextContent
from agent_framework._workflows._events import WorkflowOutputEvent
# Sequential/Concurrent workflows often output list[ChatMessage]
messages = [
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="World")]),
]
event = WorkflowOutputEvent(data=messages, source_executor_id="complete")
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
assert events[0].type == "response.output_item.added"
# =============================================================================
# WorkflowFailedEvent Tests
# =============================================================================
async def test_workflow_failed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowFailedEvent is converted to response.failed."""
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
details = WorkflowErrorDetails(
error_type="TestError",
message="Workflow failed due to test error",
executor_id="failing_executor",
)
event = WorkflowFailedEvent(details=details)
events = await mapper.convert_event(event, test_request)
# WorkflowFailedEvent should emit response.failed
assert len(events) >= 1
# Find the failed event
failed_events = [e for e in events if getattr(e, "type", "") == "response.failed"]
assert len(failed_events) == 1, f"Expected response.failed, got types: {[getattr(e, 'type', '') for e in events]}"
# Check response contains error info
response = failed_events[0].response
assert response.status == "failed"
assert response.error is not None
async def test_workflow_failed_event_with_traceback(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowFailedEvent includes traceback when available."""
from agent_framework._workflows._events import WorkflowErrorDetails, WorkflowFailedEvent
details = WorkflowErrorDetails(
error_type="ValueError",
message="Invalid input provided",
traceback="Traceback (most recent call last):\n File ...\nValueError: Invalid input",
executor_id="validation_executor",
)
event = WorkflowFailedEvent(details=details)
events = await mapper.convert_event(event, test_request)
assert len(events) == 1
assert events[0].type == "response.failed"
# =============================================================================
# WorkflowWarningEvent and WorkflowErrorEvent Tests
# =============================================================================
async def test_workflow_warning_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowWarningEvent is converted to trace event."""
from agent_framework._workflows._events import WorkflowWarningEvent
event = WorkflowWarningEvent(data="This is a warning message")
events = await mapper.convert_event(event, test_request)
# WorkflowWarningEvent should emit a trace event
assert len(events) == 1
assert events[0].type == "response.trace.completed"
assert events[0].data["event_type"] == "WorkflowWarningEvent"
async def test_workflow_error_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test WorkflowErrorEvent is converted to trace event."""
from agent_framework._workflows._events import WorkflowErrorEvent
event = WorkflowErrorEvent(data=ValueError("Something went wrong"))
events = await mapper.convert_event(event, test_request)
# WorkflowErrorEvent should emit a trace event
assert len(events) == 1
assert events[0].type == "response.trace.completed"
assert events[0].data["event_type"] == "WorkflowErrorEvent"
# =============================================================================
# RequestInfoEvent Tests (Human-in-the-Loop)
# =============================================================================
async def test_request_info_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test RequestInfoEvent is converted to HIL request event."""
from agent_framework._workflows._events import RequestInfoEvent
event = RequestInfoEvent(
request_id="req_123",
source_executor_id="approval_executor",
request_data={"action": "approve", "details": "Please approve this action"},
response_type=str,
)
events = await mapper.convert_event(event, test_request)
# RequestInfoEvent should emit response.request_info.requested
assert len(events) >= 1
# Check that request info is captured
has_hil_event = any(getattr(e, "type", "") == "response.request_info.requested" for e in events)
assert has_hil_event, f"Expected response.request_info.requested, got: {[getattr(e, 'type', '') for e in events]}"
# Verify the event contains the expected data
hil_event = [e for e in events if getattr(e, "type", "") == "response.request_info.requested"][0]
assert hil_event.request_id == "req_123"
assert hil_event.source_executor_id == "approval_executor"
# =============================================================================
# SuperStep Event Tests
# =============================================================================
async def test_superstep_started_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test SuperStepStartedEvent is handled gracefully."""
from agent_framework._workflows._events import SuperStepStartedEvent
event = SuperStepStartedEvent(iteration=1)
events = await mapper.convert_event(event, test_request)
# SuperStepStartedEvent may not emit events (internal workflow signal)
# Just ensure it doesn't crash
assert isinstance(events, list)
async def test_superstep_completed_event(mapper: MessageMapper, test_request: AgentFrameworkRequest) -> None:
"""Test SuperStepCompletedEvent is handled gracefully."""
from agent_framework._workflows._events import SuperStepCompletedEvent
event = SuperStepCompletedEvent(iteration=1)
events = await mapper.convert_event(event, test_request)
# SuperStepCompletedEvent may not emit events (internal workflow signal)
# Just ensure it doesn't crash
assert isinstance(events, list)