arcade-mcp/libs/tests/core/test_schema_validation.py
Sam Partee e188fc6ae9
Improve Typedict and Basemodel support (#523)
Improve Pydantic and Typedict support and add a bunch of tets.

1. Fixed the test failure where TypeDict was being serialized as a list
of tuples instead of a dict by:
- Adding proper handling for BaseModel instances in the output.py file
- Converting BaseModel results (from TypeDict conversion) to dicts using
model_dump()
    - Handling lists containing BaseModel objects
2. Fixed None handling to ensure None results are converted to empty
strings as expected
3. Updated the schema.py to allow dict and list types in
ToolCallOutput.value
  4. new tests
    - TypeDict output execution tests
    - Output factory tests
    - Executor tests with TypeDict support
    - Schema validation tests

  The key changes were:
- In ``arcade_core/output.py``: Added BaseModel conversion logic in the
success method
- In ``arcade_core/schema.py``: Changed ToolCallOutput.value type from
list[str] to list to support complex types


TODO
- [ ] Confirm engine compatibility without changes made to engine

---------

Co-authored-by: Eric Gustin <eric@arcade.dev>
2025-08-27 16:50:09 -07:00

196 lines
6.8 KiB
Python

"""
Tests for ToolCallOutput schema validation with complex types.
"""
import pytest
from arcade_core.schema import ToolCallError, ToolCallLog, ToolCallOutput
from pydantic import ValidationError
class TestToolCallOutputValidation:
"""Test ToolCallOutput validation with various data types."""
def test_basic_types(self):
"""Test that basic types are validated correctly."""
# String
output = ToolCallOutput(value="test string")
assert output.value == "test string"
# Integer
output = ToolCallOutput(value=42)
assert output.value == 42
# Float
output = ToolCallOutput(value=3.14)
assert output.value == 3.14
# Boolean
output = ToolCallOutput(value=True)
assert output.value is True
# None
output = ToolCallOutput(value=None)
assert output.value is None
def test_dict_types(self):
"""Test that dict types are validated correctly."""
# Simple dict
output = ToolCallOutput(value={"key": "value"})
assert output.value == {"key": "value"}
# Nested dict
output = ToolCallOutput(value={"outer": {"inner": "value"}})
assert output.value == {"outer": {"inner": "value"}}
# Empty dict
output = ToolCallOutput(value={})
assert output.value == {}
# Dict with mixed types
output = ToolCallOutput(
value={
"string": "text",
"number": 123,
"float": 45.6,
"bool": True,
"null": None,
"list": [1, 2, 3],
"dict": {"nested": "value"},
}
)
assert output.value["string"] == "text"
assert output.value["number"] == 123
assert output.value["list"] == [1, 2, 3]
def test_list_types(self):
"""Test that list types are validated correctly."""
# List of strings (original type)
output = ToolCallOutput(value=["a", "b", "c"])
assert output.value == ["a", "b", "c"]
# List of integers
output = ToolCallOutput(value=[1, 2, 3])
assert output.value == [1, 2, 3]
# List of dicts (TypedDict at runtime)
output = ToolCallOutput(value=[{"id": 1, "name": "first"}, {"id": 2, "name": "second"}])
assert output.value == [{"id": 1, "name": "first"}, {"id": 2, "name": "second"}]
# Mixed type list
output = ToolCallOutput(value=[1, "two", 3.0, True, None, {"key": "value"}])
assert len(output.value) == 6
assert output.value[5] == {"key": "value"}
# Empty list
output = ToolCallOutput(value=[])
assert output.value == []
# Nested lists
output = ToolCallOutput(value=[[1, 2], [3, 4], [5, 6]])
assert output.value == [[1, 2], [3, 4], [5, 6]]
def test_complex_nested_structures(self):
"""Test complex nested structures that might come from TypedDict."""
# Simulate a complex API response structure
complex_data = {
"status": "success",
"data": {
"users": [
{
"id": 1,
"name": "Alice",
"roles": ["admin", "user"],
"metadata": {"last_login": "2024-01-01", "active": True},
},
{
"id": 2,
"name": "Bob",
"roles": ["user"],
"metadata": {"last_login": "2024-01-02", "active": False},
},
],
"total": 2,
"page_info": {"page": 1, "per_page": 10, "has_next": False},
},
"errors": [],
}
output = ToolCallOutput(value=complex_data)
assert output.value == complex_data
assert output.value["data"]["users"][0]["name"] == "Alice"
assert output.value["data"]["page_info"]["has_next"] is False
def test_error_and_logs_with_value(self):
"""Test that error and logs can coexist with different value types."""
# With dict value and logs
output = ToolCallOutput(
value={"result": "success"},
logs=[
ToolCallLog(message="Processing started", level="info"),
ToolCallLog(message="Deprecation warning", level="warning", subtype="deprecation"),
],
)
assert output.value == {"result": "success"}
assert len(output.logs) == 2
# With list value and error
output = ToolCallOutput(
error=ToolCallError(
message="Partial failure",
developer_message="Some items failed to process",
can_retry=True,
)
)
assert output.error.message == "Partial failure"
assert output.value is None
def test_unsupported_types_still_fail(self):
"""Test that truly unsupported types still fail validation."""
# Custom object (not dict, list, or basic type)
class CustomClass:
def __init__(self):
self.data = "test"
# This should fail because CustomClass instance is not a supported type
# Note: This test is about Pydantic validation, not the output factory
# The output factory would catch this earlier
with pytest.raises(ValidationError):
# Directly creating with an unsupported type should fail
ToolCallOutput(value=CustomClass())
def test_very_large_structures(self):
"""Test that large structures are handled properly."""
# Large list of dicts
large_list = [{"id": i, "value": f"item_{i}"} for i in range(1000)]
output = ToolCallOutput(value=large_list)
assert len(output.value) == 1000
assert output.value[500]["id"] == 500
# Deeply nested structure
deep_dict = {"level1": {"level2": {"level3": {"level4": {"level5": "deep_value"}}}}}
output = ToolCallOutput(value=deep_dict)
assert output.value["level1"]["level2"]["level3"]["level4"]["level5"] == "deep_value"
def test_json_serializable(self):
"""Test that all supported types are JSON serializable."""
import json
test_cases = [
{"type": "string"},
["list", "of", "strings"],
[{"id": 1}, {"id": 2}],
{"nested": {"data": [1, 2, 3]}},
123,
45.6,
True,
None,
]
for test_value in test_cases:
output = ToolCallOutput(value=test_value)
# This should not raise an exception
json_str = json.dumps(output.model_dump())
# And we should be able to parse it back
parsed = json.loads(json_str)
assert parsed["value"] == test_value