mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
57da1bcfeb
* Updated declarative kind mapping * Fixed required property handling * Updated inline yaml sample * Fixed remaining declarative samples * Added lazy initialization for PowerFx engine * Small fix
1196 lines
42 KiB
Python
1196 lines
42 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Tests for MAML model classes."""
|
|
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from agent_framework_declarative._models import (
|
|
AgentDefinition,
|
|
AgentManifest,
|
|
AnonymousConnection,
|
|
ApiKeyConnection,
|
|
ArrayProperty,
|
|
Binding,
|
|
CodeInterpreterTool,
|
|
Connection,
|
|
CustomTool,
|
|
EnvironmentVariable,
|
|
FileSearchTool,
|
|
Format,
|
|
FunctionTool,
|
|
McpServerApprovalMode,
|
|
McpServerToolAlwaysRequireApprovalMode,
|
|
McpServerToolNeverRequireApprovalMode,
|
|
McpServerToolSpecifyApprovalMode,
|
|
McpTool,
|
|
Model,
|
|
ModelOptions,
|
|
ModelResource,
|
|
ObjectProperty,
|
|
OpenApiTool,
|
|
Parser,
|
|
PromptAgent,
|
|
Property,
|
|
PropertySchema,
|
|
ProtocolVersionRecord,
|
|
ReferenceConnection,
|
|
RemoteConnection,
|
|
Resource,
|
|
Template,
|
|
ToolResource,
|
|
WebSearchTool,
|
|
_safe_mode_context,
|
|
_try_powerfx_eval,
|
|
)
|
|
|
|
pytestmark = pytest.mark.skipif(sys.version_info >= (3, 14), reason="Skipping on Python 3.14+")
|
|
|
|
|
|
class TestBinding:
|
|
"""Tests for Binding class."""
|
|
|
|
def test_binding_creation(self):
|
|
binding = Binding(name="arg1", input="value1")
|
|
assert binding.name == "arg1"
|
|
assert binding.input == "value1"
|
|
|
|
def test_binding_from_dict(self):
|
|
data = {"name": "arg1", "input": "value1"}
|
|
binding = Binding.from_dict(data)
|
|
assert binding.name == "arg1"
|
|
assert binding.input == "value1"
|
|
|
|
def test_binding_to_dict(self):
|
|
binding = Binding(name="arg1", input="value1")
|
|
result = binding.to_dict()
|
|
assert result["name"] == "arg1"
|
|
assert result["input"] == "value1"
|
|
|
|
|
|
class TestProperty:
|
|
"""Tests for Property class."""
|
|
|
|
def test_property_creation(self):
|
|
prop = Property(
|
|
name="test_prop",
|
|
kind="string",
|
|
description="A test property",
|
|
required=True,
|
|
default="default_value",
|
|
example="example_value",
|
|
enum=["val1", "val2"],
|
|
)
|
|
assert prop.name == "test_prop"
|
|
assert prop.kind == "string"
|
|
assert prop.description == "A test property"
|
|
assert prop.required is True
|
|
assert prop.default == "default_value"
|
|
assert prop.example == "example_value"
|
|
assert prop.enum == ["val1", "val2"]
|
|
|
|
def test_property_from_dict(self):
|
|
data = {
|
|
"name": "test_prop",
|
|
"kind": "string",
|
|
"description": "A test property",
|
|
"required": True,
|
|
}
|
|
prop = Property.from_dict(data)
|
|
assert prop.name == "test_prop"
|
|
assert prop.kind == "string"
|
|
assert prop.description == "A test property"
|
|
assert prop.required is True
|
|
|
|
def test_property_from_dict_type_maps_to_kind(self):
|
|
"""Test that 'type' field in YAML is mapped to 'kind' internally."""
|
|
data = {
|
|
"name": "test_prop",
|
|
"type": "string",
|
|
"description": "A test property",
|
|
"required": True,
|
|
}
|
|
prop = Property.from_dict(data)
|
|
assert prop.name == "test_prop"
|
|
assert prop.kind == "string"
|
|
|
|
def test_property_from_dict_kind_takes_precedence_over_type(self):
|
|
"""Test that 'kind' takes precedence when both 'type' and 'kind' are present."""
|
|
data = {
|
|
"name": "test_prop",
|
|
"type": "integer",
|
|
"kind": "string",
|
|
}
|
|
prop = Property.from_dict(data)
|
|
assert prop.kind == "string"
|
|
|
|
def test_property_from_dict_type_dispatches_to_array(self):
|
|
"""Test that 'type: array' correctly dispatches to ArrayProperty."""
|
|
data = {
|
|
"name": "test_array",
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
}
|
|
prop = Property.from_dict(data)
|
|
assert isinstance(prop, ArrayProperty)
|
|
assert prop.kind == "array"
|
|
|
|
def test_property_from_dict_type_dispatches_to_object(self):
|
|
"""Test that 'type: object' correctly dispatches to ObjectProperty."""
|
|
data = {
|
|
"name": "test_object",
|
|
"type": "object",
|
|
"properties": {"field": {"type": "string"}},
|
|
}
|
|
prop = Property.from_dict(data)
|
|
assert isinstance(prop, ObjectProperty)
|
|
assert prop.kind == "object"
|
|
|
|
|
|
class TestArrayProperty:
|
|
"""Tests for ArrayProperty class."""
|
|
|
|
def test_array_property_creation(self):
|
|
items = Property(name="item", kind="string")
|
|
array_prop = ArrayProperty(name="test_array", kind="array", items=items, required=True)
|
|
assert array_prop.name == "test_array"
|
|
assert array_prop.kind == "array"
|
|
assert array_prop.items.name == "item"
|
|
assert array_prop.required is True
|
|
|
|
def test_array_property_from_dict(self):
|
|
data = {
|
|
"name": "test_array",
|
|
"kind": "array",
|
|
"items": {"name": "item", "kind": "string"},
|
|
"required": True,
|
|
}
|
|
array_prop = ArrayProperty.from_dict(data)
|
|
assert array_prop.name == "test_array"
|
|
assert array_prop.kind == "array"
|
|
assert isinstance(array_prop.items, Property)
|
|
assert array_prop.items.name == "item"
|
|
|
|
|
|
class TestObjectProperty:
|
|
"""Tests for ObjectProperty class."""
|
|
|
|
def test_object_property_creation(self):
|
|
props = [
|
|
Property(name="prop1", kind="string"),
|
|
Property(name="prop2", kind="integer"),
|
|
]
|
|
obj_prop = ObjectProperty(name="test_object", kind="object", properties=props, required=True)
|
|
assert obj_prop.name == "test_object"
|
|
assert obj_prop.kind == "object"
|
|
assert len(obj_prop.properties) == 2
|
|
assert obj_prop.properties[0].name == "prop1"
|
|
|
|
def test_object_property_from_dict(self):
|
|
data = {
|
|
"name": "test_object",
|
|
"kind": "object",
|
|
"properties": [
|
|
{"name": "prop1", "kind": "string"},
|
|
{"name": "prop2", "kind": "integer"},
|
|
],
|
|
"required": True,
|
|
}
|
|
obj_prop = ObjectProperty.from_dict(data)
|
|
assert obj_prop.name == "test_object"
|
|
assert obj_prop.kind == "object"
|
|
assert len(obj_prop.properties) == 2
|
|
assert all(isinstance(p, Property) for p in obj_prop.properties)
|
|
|
|
def test_object_property_with_dict_properties(self):
|
|
"""Test ObjectProperty with dict format for properties (MAML YAML dict syntax)."""
|
|
data = {
|
|
"name": "person",
|
|
"kind": "object",
|
|
"properties": {
|
|
"name": {"kind": "string", "required": True},
|
|
"email": {"kind": "string"},
|
|
"age": {"kind": "integer"},
|
|
},
|
|
}
|
|
obj_prop = ObjectProperty.from_dict(data)
|
|
assert obj_prop.name == "person"
|
|
assert obj_prop.kind == "object"
|
|
assert len(obj_prop.properties) == 3
|
|
|
|
# Check that all properties were converted correctly
|
|
prop_names = {p.name for p in obj_prop.properties}
|
|
assert prop_names == {"name", "email", "age"}
|
|
|
|
# Check specific property
|
|
name_prop = next(p for p in obj_prop.properties if p.name == "name")
|
|
assert name_prop.kind == "string"
|
|
assert name_prop.required is True
|
|
|
|
|
|
class TestPropertySchema:
|
|
"""Tests for PropertySchema class."""
|
|
|
|
def test_property_schema_creation(self):
|
|
props = [Property(name="prop1", kind="string")]
|
|
schema = PropertySchema(properties=props, strict=True)
|
|
assert schema.strict is True
|
|
assert len(schema.properties) == 1
|
|
|
|
def test_property_schema_from_dict(self):
|
|
data = {
|
|
"strict": False,
|
|
"properties": [{"name": "prop1", "kind": "string"}],
|
|
}
|
|
schema = PropertySchema.from_dict(data)
|
|
assert schema.strict is False
|
|
assert len(schema.properties) == 1
|
|
# Properties are properly converted to Property instances
|
|
assert isinstance(schema.properties[0], Property)
|
|
assert schema.properties[0].name == "prop1"
|
|
assert schema.properties[0].kind == "string"
|
|
|
|
def test_property_schema_with_dict_properties(self):
|
|
"""Test PropertySchema with dict format for properties (MAML YAML dict syntax)."""
|
|
data = {
|
|
"strict": True,
|
|
"properties": {
|
|
"firstName": {"kind": "string", "description": "First name"},
|
|
"lastName": {"kind": "string", "description": "Last name"},
|
|
"age": {"kind": "integer", "required": True},
|
|
},
|
|
}
|
|
schema = PropertySchema.from_dict(data)
|
|
assert schema.strict is True
|
|
assert len(schema.properties) == 3
|
|
|
|
# Check that all properties were converted correctly
|
|
prop_names = {p.name for p in schema.properties}
|
|
assert prop_names == {"firstName", "lastName", "age"}
|
|
|
|
# Check specific property details
|
|
age_prop = next(p for p in schema.properties if p.name == "age")
|
|
assert age_prop.kind == "integer"
|
|
assert age_prop.required is True
|
|
|
|
def test_property_schema_with_type_field_produces_correct_json_schema(self):
|
|
"""Test that PropertySchema with 'type' fields (YAML spec format) produces valid JSON schema."""
|
|
data = {
|
|
"properties": {
|
|
"language": {"type": "string", "required": True, "description": "The language."},
|
|
"answer": {"type": "string", "required": False, "description": "The answer."},
|
|
},
|
|
}
|
|
schema = PropertySchema.from_dict(data)
|
|
assert len(schema.properties) == 2
|
|
|
|
lang_prop = next(p for p in schema.properties if p.name == "language")
|
|
assert lang_prop.kind == "string"
|
|
|
|
json_schema = schema.to_json_schema()
|
|
assert json_schema["type"] == "object"
|
|
assert json_schema["properties"]["language"]["type"] == "string"
|
|
assert json_schema["properties"]["answer"]["type"] == "string"
|
|
# required is a top-level array, not a per-property boolean
|
|
assert json_schema["required"] == ["language"]
|
|
assert "required" not in json_schema["properties"]["language"]
|
|
assert "required" not in json_schema["properties"]["answer"]
|
|
|
|
|
|
class TestConnection:
|
|
"""Tests for Connection base class."""
|
|
|
|
def test_connection_creation(self):
|
|
conn = Connection(kind="base")
|
|
assert conn.kind == "base"
|
|
|
|
def test_connection_from_dict(self):
|
|
data = {"kind": "base"}
|
|
conn = Connection.from_dict(data)
|
|
assert conn.kind == "base"
|
|
|
|
|
|
class TestReferenceConnection:
|
|
"""Tests for ReferenceConnection class."""
|
|
|
|
def test_reference_connection_creation(self):
|
|
conn = ReferenceConnection(name="my-connection", target="target-connection")
|
|
assert conn.kind == "reference"
|
|
assert conn.name == "my-connection"
|
|
assert conn.target == "target-connection"
|
|
|
|
def test_reference_connection_from_dict(self):
|
|
data = {"kind": "reference", "name": "my-connection", "target": "target-connection"}
|
|
conn = ReferenceConnection.from_dict(data)
|
|
assert conn.kind == "reference"
|
|
assert conn.name == "my-connection"
|
|
assert conn.target == "target-connection"
|
|
|
|
|
|
class TestRemoteConnection:
|
|
"""Tests for RemoteConnection class."""
|
|
|
|
def test_remote_connection_creation(self):
|
|
conn = RemoteConnection(name="my-remote", endpoint="https://api.example.com")
|
|
assert conn.kind == "remote"
|
|
assert conn.endpoint == "https://api.example.com"
|
|
|
|
def test_remote_connection_from_dict(self):
|
|
data = {"kind": "remote", "endpoint": "https://api.example.com"}
|
|
conn = RemoteConnection.from_dict(data)
|
|
assert conn.kind == "remote"
|
|
assert conn.endpoint == "https://api.example.com"
|
|
|
|
|
|
class TestApiKeyConnection:
|
|
"""Tests for ApiKeyConnection class."""
|
|
|
|
def test_api_key_connection_creation(self):
|
|
conn = ApiKeyConnection(apiKey="secret-key", endpoint="https://api.example.com")
|
|
assert conn.kind == "key"
|
|
assert conn.apiKey == "secret-key"
|
|
assert conn.endpoint == "https://api.example.com"
|
|
|
|
def test_api_key_connection_from_dict(self):
|
|
data = {"kind": "key", "apiKey": "secret-key", "endpoint": "https://api.example.com"}
|
|
conn = ApiKeyConnection.from_dict(data)
|
|
assert conn.kind == "key"
|
|
assert conn.apiKey == "secret-key"
|
|
|
|
|
|
class TestAnonymousConnection:
|
|
"""Tests for AnonymousConnection class."""
|
|
|
|
def test_anonymous_connection_creation(self):
|
|
conn = AnonymousConnection(endpoint="https://api.example.com")
|
|
assert conn.kind == "anonymous"
|
|
assert conn.endpoint == "https://api.example.com"
|
|
|
|
def test_anonymous_connection_from_dict(self):
|
|
data = {"kind": "anonymous", "endpoint": "https://api.example.com"}
|
|
conn = AnonymousConnection.from_dict(data)
|
|
assert conn.kind == "anonymous"
|
|
assert conn.endpoint == "https://api.example.com"
|
|
|
|
|
|
class TestModelOptions:
|
|
"""Tests for ModelOptions class."""
|
|
|
|
def test_model_options_creation(self):
|
|
options = ModelOptions(temperature=0.7, maxOutputTokens=1000, topP=0.9)
|
|
assert options.temperature == 0.7
|
|
assert options.maxOutputTokens == 1000
|
|
assert options.topP == 0.9
|
|
|
|
def test_model_options_from_dict(self):
|
|
data = {"temperature": 0.7, "maxOutputTokens": 1000, "topP": 0.9}
|
|
options = ModelOptions.from_dict(data)
|
|
assert options.temperature == 0.7
|
|
assert options.maxOutputTokens == 1000
|
|
assert options.topP == 0.9
|
|
|
|
|
|
class TestModel:
|
|
"""Tests for Model class."""
|
|
|
|
def test_model_creation(self):
|
|
model = Model(id="gpt-4", provider="openai")
|
|
assert model.id == "gpt-4"
|
|
assert model.provider == "openai"
|
|
|
|
def test_model_from_dict(self):
|
|
data = {"id": "gpt-4", "provider": "openai"}
|
|
model = Model.from_dict(data)
|
|
assert model.id == "gpt-4"
|
|
assert model.provider == "openai"
|
|
|
|
def test_model_with_connection(self):
|
|
data = {
|
|
"id": "gpt-4",
|
|
"connection": {"kind": "reference", "name": "my-connection"},
|
|
}
|
|
model = Model.from_dict(data)
|
|
assert model.id == "gpt-4"
|
|
assert model.connection.kind == "reference"
|
|
|
|
|
|
class TestFormat:
|
|
"""Tests for Format class."""
|
|
|
|
def test_format_creation(self):
|
|
fmt = Format(kind="json", strict=True, options={"type": "object"})
|
|
assert fmt.kind == "json"
|
|
assert fmt.strict is True
|
|
assert fmt.options == {"type": "object"}
|
|
|
|
def test_format_from_dict(self):
|
|
data = {"kind": "json", "strict": False, "options": {"type": "object"}}
|
|
fmt = Format.from_dict(data)
|
|
assert fmt.kind == "json"
|
|
assert fmt.strict is False
|
|
|
|
|
|
class TestParser:
|
|
"""Tests for Parser class."""
|
|
|
|
def test_parser_creation(self):
|
|
parser = Parser(kind="json", options={"strict": True})
|
|
assert parser.kind == "json"
|
|
assert parser.options == {"strict": True}
|
|
|
|
def test_parser_from_dict(self):
|
|
data = {"kind": "json", "options": {"strict": True}}
|
|
parser = Parser.from_dict(data)
|
|
assert parser.kind == "json"
|
|
assert parser.options == {"strict": True}
|
|
|
|
|
|
class TestTemplate:
|
|
"""Tests for Template class."""
|
|
|
|
def test_template_creation(self):
|
|
template = Template(
|
|
format=Format(kind="text"),
|
|
parser=Parser(kind="text"),
|
|
)
|
|
assert isinstance(template.format, Format)
|
|
assert isinstance(template.parser, Parser)
|
|
|
|
def test_template_from_dict(self):
|
|
data = {
|
|
"format": {"kind": "text"},
|
|
"parser": {"kind": "text"},
|
|
}
|
|
template = Template.from_dict(data)
|
|
assert isinstance(template.format, Format)
|
|
assert isinstance(template.parser, Parser)
|
|
|
|
|
|
class TestAgentDefinition:
|
|
"""Tests for AgentDefinition class."""
|
|
|
|
def test_agent_definition_creation(self):
|
|
agent = AgentDefinition(
|
|
name="test-agent",
|
|
description="A test agent",
|
|
)
|
|
assert agent.name == "test-agent"
|
|
assert agent.description == "A test agent"
|
|
|
|
def test_agent_definition_from_dict(self):
|
|
data = {
|
|
"name": "test-agent",
|
|
"description": "A test agent",
|
|
}
|
|
agent = AgentDefinition.from_dict(data)
|
|
assert agent.name == "test-agent"
|
|
assert agent.description == "A test agent"
|
|
|
|
|
|
class TestFunctionTool:
|
|
"""Tests for FunctionTool class."""
|
|
|
|
def test_function_tool_creation(self):
|
|
tool = FunctionTool(
|
|
name="my_function",
|
|
description="A test function",
|
|
kind="function",
|
|
)
|
|
assert tool.name == "my_function"
|
|
assert tool.kind == "function"
|
|
|
|
def test_function_tool_from_dict(self):
|
|
data = {
|
|
"name": "my_function",
|
|
"description": "A test function",
|
|
"kind": "function",
|
|
"strict": False,
|
|
}
|
|
tool = FunctionTool.from_dict(data)
|
|
assert tool.name == "my_function"
|
|
assert tool.kind == "function"
|
|
|
|
def test_function_tool_with_dict_bindings(self):
|
|
"""Test FunctionTool with dict format for bindings (MAML YAML dict syntax)."""
|
|
data = {
|
|
"name": "calculate",
|
|
"kind": "function",
|
|
"description": "Calculate something",
|
|
"bindings": {
|
|
"x": "input.x",
|
|
"y": "input.y",
|
|
"operation": "input.op",
|
|
},
|
|
}
|
|
tool = FunctionTool.from_dict(data)
|
|
assert tool.name == "calculate"
|
|
assert len(tool.bindings) == 3
|
|
|
|
# Check that all bindings were converted correctly
|
|
binding_names = {b.name for b in tool.bindings}
|
|
assert binding_names == {"x", "y", "operation"}
|
|
|
|
# Check specific binding
|
|
x_binding = next(b for b in tool.bindings if b.name == "x")
|
|
assert x_binding.input == "input.x"
|
|
|
|
|
|
class TestCustomTool:
|
|
"""Tests for CustomTool class."""
|
|
|
|
def test_custom_tool_creation(self):
|
|
tool = CustomTool(
|
|
name="custom_tool",
|
|
description="A custom tool",
|
|
kind="custom",
|
|
options={"endpoint": "https://tool.example.com"},
|
|
)
|
|
assert tool.name == "custom_tool"
|
|
assert tool.kind == "custom"
|
|
assert tool.options == {"endpoint": "https://tool.example.com"}
|
|
|
|
def test_custom_tool_from_dict(self):
|
|
data = {
|
|
"name": "custom_tool",
|
|
"description": "A custom tool",
|
|
"kind": "custom",
|
|
"options": {"endpoint": "https://tool.example.com"},
|
|
}
|
|
tool = CustomTool.from_dict(data)
|
|
assert tool.name == "custom_tool"
|
|
assert tool.kind == "custom"
|
|
|
|
|
|
class TestWebSearchTool:
|
|
"""Tests for WebSearchTool class."""
|
|
|
|
def test_web_search_tool_creation(self):
|
|
tool = WebSearchTool(
|
|
name="web_search",
|
|
description="Search the web",
|
|
kind="web_search",
|
|
options={"maxResults": 10},
|
|
)
|
|
assert tool.name == "web_search"
|
|
assert tool.kind == "web_search"
|
|
assert tool.options == {"maxResults": 10}
|
|
|
|
def test_web_search_tool_from_dict(self):
|
|
data = {
|
|
"name": "web_search",
|
|
"description": "Search the web",
|
|
"kind": "web_search",
|
|
"options": {"maxResults": 10},
|
|
}
|
|
tool = WebSearchTool.from_dict(data)
|
|
assert tool.name == "web_search"
|
|
assert tool.kind == "web_search"
|
|
assert tool.options == {"maxResults": 10}
|
|
|
|
|
|
class TestFileSearchTool:
|
|
"""Tests for FileSearchTool class."""
|
|
|
|
def test_file_search_tool_creation(self):
|
|
tool = FileSearchTool(
|
|
name="file_search",
|
|
description="Search files",
|
|
kind="file_search",
|
|
vectorStoreIds=["vs1", "vs2"],
|
|
)
|
|
assert tool.name == "file_search"
|
|
assert tool.kind == "file_search"
|
|
assert tool.vectorStoreIds == ["vs1", "vs2"]
|
|
|
|
def test_file_search_tool_from_dict(self):
|
|
data = {
|
|
"name": "file_search",
|
|
"description": "Search files",
|
|
"kind": "file_search",
|
|
"vectorStoreIds": ["vs1", "vs2"],
|
|
}
|
|
tool = FileSearchTool.from_dict(data)
|
|
assert tool.name == "file_search"
|
|
assert tool.kind == "file_search"
|
|
assert tool.vectorStoreIds == ["vs1", "vs2"]
|
|
|
|
|
|
class TestMcpServerApprovalMode:
|
|
"""Tests for MCP Server Approval Mode classes."""
|
|
|
|
def test_always_approval_mode(self):
|
|
mode = McpServerToolAlwaysRequireApprovalMode()
|
|
assert mode.kind == "always"
|
|
|
|
def test_always_approval_mode_from_dict(self):
|
|
data = {"kind": "always"}
|
|
mode = McpServerToolAlwaysRequireApprovalMode.from_dict(data)
|
|
assert mode.kind == "always"
|
|
|
|
def test_never_approval_mode(self):
|
|
mode = McpServerToolNeverRequireApprovalMode()
|
|
assert mode.kind == "never"
|
|
|
|
def test_never_approval_mode_from_dict(self):
|
|
data = {"kind": "never"}
|
|
mode = McpServerToolNeverRequireApprovalMode.from_dict(data)
|
|
assert mode.kind == "never"
|
|
|
|
def test_specify_approval_mode(self):
|
|
mode = McpServerToolSpecifyApprovalMode(
|
|
alwaysRequireApprovalTools=["tool1"],
|
|
neverRequireApprovalTools=["tool2"],
|
|
)
|
|
assert mode.kind == "specify"
|
|
assert mode.alwaysRequireApprovalTools == ["tool1"]
|
|
assert mode.neverRequireApprovalTools == ["tool2"]
|
|
|
|
def test_specify_approval_mode_from_dict(self):
|
|
data = {
|
|
"kind": "specify",
|
|
"alwaysRequireApprovalTools": ["tool1"],
|
|
"neverRequireApprovalTools": ["tool2"],
|
|
}
|
|
mode = McpServerToolSpecifyApprovalMode.from_dict(data)
|
|
assert mode.kind == "specify"
|
|
assert mode.alwaysRequireApprovalTools == ["tool1"]
|
|
assert mode.neverRequireApprovalTools == ["tool2"]
|
|
|
|
|
|
class TestMcpTool:
|
|
"""Tests for McpTool class."""
|
|
|
|
def test_mcp_tool_creation(self):
|
|
tool = McpTool(
|
|
name="mcp_tool",
|
|
description="An MCP tool",
|
|
kind="mcp",
|
|
serverName="test-server",
|
|
)
|
|
assert tool.name == "mcp_tool"
|
|
assert tool.kind == "mcp"
|
|
assert tool.serverName == "test-server"
|
|
|
|
def test_mcp_tool_from_dict(self):
|
|
data = {
|
|
"name": "mcp_tool",
|
|
"description": "An MCP tool",
|
|
"kind": "mcp",
|
|
"serverName": "test-server",
|
|
"approvalMode": {"kind": "always"},
|
|
}
|
|
tool = McpTool.from_dict(data)
|
|
assert tool.name == "mcp_tool"
|
|
assert tool.kind == "mcp"
|
|
assert isinstance(tool.approvalMode, McpServerApprovalMode)
|
|
|
|
def test_mcp_tool_with_simplified_approval_mode(self):
|
|
"""Test McpTool with simplified string format for approvalMode."""
|
|
# Test simplified string format: approvalMode: "always"
|
|
data = {
|
|
"name": "mcp_tool",
|
|
"description": "An MCP tool",
|
|
"kind": "mcp",
|
|
"serverName": "test-server",
|
|
"approvalMode": "always",
|
|
}
|
|
tool = McpTool.from_dict(data)
|
|
assert tool.name == "mcp_tool"
|
|
assert tool.kind == "mcp"
|
|
assert isinstance(tool.approvalMode, McpServerApprovalMode)
|
|
assert tool.approvalMode.kind == "always"
|
|
|
|
def test_mcp_tool_approval_mode_equivalence(self):
|
|
"""Test that simplified and full format produce equivalent results."""
|
|
# Simplified format
|
|
data_simplified = {
|
|
"name": "mcp_tool",
|
|
"kind": "mcp",
|
|
"approvalMode": "never",
|
|
}
|
|
tool_simplified = McpTool.from_dict(data_simplified)
|
|
|
|
# Full format
|
|
data_full = {
|
|
"name": "mcp_tool",
|
|
"kind": "mcp",
|
|
"approvalMode": {"kind": "never"},
|
|
}
|
|
tool_full = McpTool.from_dict(data_full)
|
|
|
|
# Both should produce the same result
|
|
assert tool_simplified.approvalMode.kind == tool_full.approvalMode.kind
|
|
assert tool_simplified.approvalMode.kind == "never"
|
|
|
|
|
|
class TestOpenApiTool:
|
|
"""Tests for OpenApiTool class."""
|
|
|
|
def test_openapi_tool_creation(self):
|
|
tool = OpenApiTool(
|
|
name="openapi_tool",
|
|
description="An OpenAPI tool",
|
|
kind="openapi",
|
|
specification="https://api.example.com/openapi.json",
|
|
)
|
|
assert tool.name == "openapi_tool"
|
|
assert tool.kind == "openapi"
|
|
assert tool.specification == "https://api.example.com/openapi.json"
|
|
|
|
def test_openapi_tool_from_dict(self):
|
|
data = {
|
|
"name": "openapi_tool",
|
|
"description": "An OpenAPI tool",
|
|
"kind": "openapi",
|
|
"specification": "https://api.example.com/openapi.json",
|
|
}
|
|
tool = OpenApiTool.from_dict(data)
|
|
assert tool.name == "openapi_tool"
|
|
assert tool.kind == "openapi"
|
|
|
|
|
|
class TestCodeInterpreterTool:
|
|
"""Tests for CodeInterpreterTool class."""
|
|
|
|
def test_code_interpreter_tool_creation(self):
|
|
tool = CodeInterpreterTool(
|
|
name="code_interpreter",
|
|
description="Execute code",
|
|
kind="code_interpreter",
|
|
fileIds=["file1", "file2"],
|
|
)
|
|
assert tool.name == "code_interpreter"
|
|
assert tool.kind == "code_interpreter"
|
|
assert tool.fileIds == ["file1", "file2"]
|
|
|
|
def test_code_interpreter_tool_from_dict(self):
|
|
data = {
|
|
"name": "code_interpreter",
|
|
"description": "Execute code",
|
|
"kind": "code_interpreter",
|
|
"fileIds": ["file1", "file2"],
|
|
}
|
|
tool = CodeInterpreterTool.from_dict(data)
|
|
assert tool.name == "code_interpreter"
|
|
assert tool.kind == "code_interpreter"
|
|
assert tool.fileIds == ["file1", "file2"]
|
|
|
|
|
|
class TestPromptAgent:
|
|
"""Tests for PromptAgent class."""
|
|
|
|
def test_prompt_agent_creation(self):
|
|
agent = PromptAgent(
|
|
name="prompt-agent",
|
|
description="A prompt-based agent",
|
|
instructions="You are a helpful assistant",
|
|
kind="Prompt",
|
|
)
|
|
assert agent.name == "prompt-agent"
|
|
assert agent.kind == "Prompt"
|
|
assert agent.instructions == "You are a helpful assistant"
|
|
|
|
def test_prompt_agent_from_dict(self):
|
|
data = {
|
|
"name": "prompt-agent",
|
|
"description": "A prompt-based agent",
|
|
"instructions": "You are a helpful assistant",
|
|
"kind": "Prompt",
|
|
"model": {"id": "gpt-4"},
|
|
}
|
|
agent = PromptAgent.from_dict(data)
|
|
assert agent.name == "prompt-agent"
|
|
assert isinstance(agent.model, Model)
|
|
assert isinstance(agent.model, Model)
|
|
|
|
def test_prompt_agent_with_tools(self):
|
|
data = {
|
|
"name": "prompt-agent",
|
|
"kind": "Prompt",
|
|
"tools": [
|
|
{"name": "search", "kind": "web_search"},
|
|
{"name": "calc", "kind": "function"},
|
|
],
|
|
}
|
|
agent = PromptAgent.from_dict(data)
|
|
assert len(agent.tools) == 2
|
|
# Tools are converted via Tool.from_dict, type depends on 'kind'
|
|
assert agent.tools[0].kind == "web_search"
|
|
assert agent.tools[1].kind == "function"
|
|
|
|
|
|
class TestResource:
|
|
"""Tests for Resource base class."""
|
|
|
|
def test_resource_creation(self):
|
|
resource = Resource(name="test-resource", kind="Resource")
|
|
assert resource.name == "test-resource"
|
|
assert resource.kind == "Resource"
|
|
|
|
def test_resource_from_dict(self):
|
|
data = {"name": "test-resource", "kind": "Resource"}
|
|
resource = Resource.from_dict(data)
|
|
assert resource.name == "test-resource"
|
|
|
|
|
|
class TestModelResource:
|
|
"""Tests for ModelResource class."""
|
|
|
|
def test_model_resource_creation(self):
|
|
resource = ModelResource(name="my-model", kind="model", id="gpt-4")
|
|
assert resource.name == "my-model"
|
|
assert resource.kind == "model"
|
|
assert resource.id == "gpt-4"
|
|
|
|
def test_model_resource_from_dict(self):
|
|
data = {
|
|
"name": "my-model",
|
|
"kind": "model",
|
|
"id": "gpt-4",
|
|
}
|
|
resource = ModelResource.from_dict(data)
|
|
assert resource.name == "my-model"
|
|
assert resource.kind == "model"
|
|
assert resource.id == "gpt-4"
|
|
|
|
|
|
class TestToolResource:
|
|
"""Tests for ToolResource class."""
|
|
|
|
def test_tool_resource_creation(self):
|
|
resource = ToolResource(name="my-tool", kind="tool", id="search-tool")
|
|
assert resource.name == "my-tool"
|
|
assert resource.kind == "tool"
|
|
assert resource.id == "search-tool"
|
|
|
|
def test_tool_resource_from_dict(self):
|
|
data = {
|
|
"name": "my-tool",
|
|
"kind": "tool",
|
|
"id": "search-tool",
|
|
}
|
|
resource = ToolResource.from_dict(data)
|
|
assert resource.name == "my-tool"
|
|
assert resource.kind == "tool"
|
|
assert resource.id == "search-tool"
|
|
|
|
|
|
class TestProtocolVersionRecord:
|
|
"""Tests for ProtocolVersionRecord class."""
|
|
|
|
def test_protocol_version_record_creation(self):
|
|
record = ProtocolVersionRecord(protocol="mcp", version="1.0.0")
|
|
assert record.protocol == "mcp"
|
|
assert record.version == "1.0.0"
|
|
|
|
def test_protocol_version_record_from_dict(self):
|
|
data = {"protocol": "mcp", "version": "1.0.0"}
|
|
record = ProtocolVersionRecord.from_dict(data)
|
|
assert record.protocol == "mcp"
|
|
assert record.version == "1.0.0"
|
|
|
|
|
|
class TestEnvironmentVariable:
|
|
"""Tests for EnvironmentVariable class."""
|
|
|
|
def test_environment_variable_creation(self):
|
|
env_var = EnvironmentVariable(name="API_KEY", value="secret123")
|
|
assert env_var.name == "API_KEY"
|
|
assert env_var.value == "secret123"
|
|
|
|
def test_environment_variable_from_dict(self):
|
|
data = {"name": "API_KEY", "value": "secret123"}
|
|
env_var = EnvironmentVariable.from_dict(data)
|
|
assert env_var.name == "API_KEY"
|
|
assert env_var.value == "secret123"
|
|
|
|
|
|
# Check if PowerFx is available
|
|
try:
|
|
from powerfx import Engine as _PfxEngine
|
|
|
|
_PfxEngine()
|
|
_powerfx_available = True
|
|
except (ImportError, RuntimeError):
|
|
_powerfx_available = False
|
|
|
|
|
|
class TestTryPowerfxEval:
|
|
"""Tests for _try_powerfx_eval function."""
|
|
|
|
def test_no_evaluation_without_equals_prefix(self):
|
|
"""Test that strings without '=' prefix are returned as-is."""
|
|
assert _try_powerfx_eval("hello") == "hello"
|
|
assert _try_powerfx_eval("test value") == "test value"
|
|
assert _try_powerfx_eval("123") == "123"
|
|
|
|
def test_none_value_returns_none(self):
|
|
"""Test that None values are returned as None."""
|
|
assert _try_powerfx_eval(None) is None
|
|
|
|
def test_empty_string_returns_empty(self):
|
|
"""Test that empty strings are returned as empty."""
|
|
assert _try_powerfx_eval("") == ""
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_simple_powerfx_expressions(self):
|
|
"""Test simple PowerFx expressions."""
|
|
from decimal import Decimal
|
|
|
|
# Simple math - returns Decimal
|
|
assert _try_powerfx_eval("=1 + 2") == Decimal("3")
|
|
assert _try_powerfx_eval("=10 * 5") == Decimal("50")
|
|
|
|
# String literals
|
|
assert _try_powerfx_eval('="hello"') == "hello"
|
|
assert _try_powerfx_eval('="test value"') == "test value"
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_env_variable_access(self, monkeypatch):
|
|
"""Test accessing environment variables using =Env.<name> pattern."""
|
|
# Set up test environment variables
|
|
monkeypatch.setenv("TEST_VAR", "test_value")
|
|
monkeypatch.setenv("API_KEY", "secret123")
|
|
monkeypatch.setenv("PORT", "8080")
|
|
|
|
# Set safe_mode=False to allow environment variable access
|
|
token = _safe_mode_context.set(False)
|
|
try:
|
|
# Test basic env access
|
|
assert _try_powerfx_eval("=Env.TEST_VAR") == "test_value"
|
|
assert _try_powerfx_eval("=Env.API_KEY") == "secret123"
|
|
assert _try_powerfx_eval("=Env.PORT") == "8080"
|
|
finally:
|
|
_safe_mode_context.reset(token)
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_env_variable_with_string_concatenation(self, monkeypatch):
|
|
"""Test env variables with string concatenation operator."""
|
|
monkeypatch.setenv("BASE_URL", "https://api.example.com")
|
|
monkeypatch.setenv("API_VERSION", "v1")
|
|
|
|
# Set safe_mode=False to allow environment variable access
|
|
token = _safe_mode_context.set(False)
|
|
try:
|
|
# Test concatenation with &
|
|
result = _try_powerfx_eval('=Env.BASE_URL & "/" & Env.API_VERSION')
|
|
assert result == "https://api.example.com/v1"
|
|
|
|
# Test concatenation with literals
|
|
result = _try_powerfx_eval('="API Key: " & Env.API_VERSION')
|
|
assert result == "API Key: v1"
|
|
finally:
|
|
_safe_mode_context.reset(token)
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_string_comparison_operators(self, monkeypatch):
|
|
"""Test PowerFx string comparison operators."""
|
|
monkeypatch.setenv("ENV_MODE", "production")
|
|
|
|
# Set safe_mode=False to allow environment variable access
|
|
token = _safe_mode_context.set(False)
|
|
try:
|
|
# Equal to - returns bool
|
|
assert _try_powerfx_eval('=Env.ENV_MODE = "production"') is True
|
|
assert _try_powerfx_eval('=Env.ENV_MODE = "development"') is False
|
|
|
|
# Not equal to - returns bool
|
|
assert _try_powerfx_eval('=Env.ENV_MODE <> "development"') is True
|
|
assert _try_powerfx_eval('=Env.ENV_MODE <> "production"') is False
|
|
finally:
|
|
_safe_mode_context.reset(token)
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_string_in_operator(self):
|
|
"""Test PowerFx 'in' operator for substring testing (case-insensitive)."""
|
|
# Substring test - case insensitive - returns bool
|
|
assert _try_powerfx_eval('="the" in "The keyboard and the monitor"') is True
|
|
assert _try_powerfx_eval('="THE" in "The keyboard and the monitor"') is True
|
|
assert _try_powerfx_eval('="xyz" in "The keyboard and the monitor"') is False
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_string_exactin_operator(self):
|
|
"""Test PowerFx 'exactin' operator for substring testing (case-sensitive)."""
|
|
# Substring test - case sensitive - returns bool
|
|
assert _try_powerfx_eval('="Windows" exactin "To display windows in the Windows operating system"') is True
|
|
assert _try_powerfx_eval('="windows" exactin "To display windows in the Windows operating system"') is True
|
|
assert _try_powerfx_eval('="WINDOWS" exactin "To display windows in the Windows operating system"') is False
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_logical_operators_with_strings(self):
|
|
"""Test PowerFx logical operators (And, Or, Not) with string comparisons."""
|
|
# And operator - returns bool
|
|
assert _try_powerfx_eval('="a" = "a" And "b" = "b"') is True
|
|
assert _try_powerfx_eval('="a" = "a" And "b" = "c"') is False
|
|
|
|
# && operator (alternative syntax) - returns bool
|
|
assert _try_powerfx_eval('="a" = "a" && "b" = "b"') is True
|
|
|
|
# Or operator - returns bool
|
|
assert _try_powerfx_eval('="a" = "b" Or "c" = "c"') is True
|
|
assert _try_powerfx_eval('="a" = "b" Or "c" = "d"') is False
|
|
|
|
# || operator (alternative syntax) - returns bool
|
|
assert _try_powerfx_eval('="a" = "b" || "c" = "c"') is True
|
|
|
|
# Not operator - returns bool
|
|
assert _try_powerfx_eval('=Not("a" = "b")') is True
|
|
assert _try_powerfx_eval('=Not("a" = "a")') is False
|
|
|
|
# ! operator (alternative syntax) - returns bool
|
|
assert _try_powerfx_eval('=!("a" = "b")') is True
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_parentheses_for_precedence(self):
|
|
"""Test using parentheses to control operator precedence."""
|
|
from decimal import Decimal
|
|
|
|
# Test arithmetic precedence - returns Decimal
|
|
assert _try_powerfx_eval("=(1 + 2) * 3") == Decimal("9")
|
|
assert _try_powerfx_eval("=1 + 2 * 3") == Decimal("7")
|
|
|
|
# Test logical precedence - returns bool
|
|
result = _try_powerfx_eval('=("a" = "a" Or "b" = "c") And "d" = "d"')
|
|
assert result is True
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_env_with_special_characters(self, monkeypatch):
|
|
"""Test env variables containing special characters in values."""
|
|
monkeypatch.setenv("URL_WITH_QUERY", "https://example.com?param=value")
|
|
monkeypatch.setenv("PATH_WITH_SPACES", "C:\\Program Files\\App")
|
|
|
|
# Set safe_mode=False to allow environment variable access
|
|
token = _safe_mode_context.set(False)
|
|
try:
|
|
result = _try_powerfx_eval("=Env.URL_WITH_QUERY")
|
|
assert result == "https://example.com?param=value"
|
|
|
|
result = _try_powerfx_eval("=Env.PATH_WITH_SPACES")
|
|
assert result == "C:\\Program Files\\App"
|
|
finally:
|
|
_safe_mode_context.reset(token)
|
|
|
|
def test_safe_mode_blocks_env_access(self, monkeypatch):
|
|
"""Test that safe_mode=True (default) blocks environment variable access."""
|
|
monkeypatch.setenv("SECRET_VAR", "secret_value")
|
|
|
|
# Set safe_mode=True (default)
|
|
token = _safe_mode_context.set(True)
|
|
try:
|
|
# When safe_mode=True, Env is not available and the expression fails,
|
|
# returning the original value
|
|
result = _try_powerfx_eval("=Env.SECRET_VAR")
|
|
assert result == "=Env.SECRET_VAR"
|
|
finally:
|
|
_safe_mode_context.reset(token)
|
|
|
|
@pytest.mark.skipif(not _powerfx_available, reason="PowerFx engine not available")
|
|
def test_safe_mode_context_isolation(self, monkeypatch):
|
|
"""Test that safe_mode context variable properly isolates env access."""
|
|
monkeypatch.setenv("TEST_VAR", "test_value")
|
|
|
|
# First, set safe_mode=True - should NOT allow env access
|
|
token = _safe_mode_context.set(True)
|
|
try:
|
|
result_safe = _try_powerfx_eval("=Env.TEST_VAR")
|
|
assert result_safe == "=Env.TEST_VAR"
|
|
|
|
# Then, set safe_mode=False - should allow env access
|
|
token2 = _safe_mode_context.set(False)
|
|
try:
|
|
result_unsafe = _try_powerfx_eval("=Env.TEST_VAR")
|
|
assert result_unsafe == "test_value"
|
|
finally:
|
|
_safe_mode_context.reset(token2)
|
|
|
|
# After reset, should block again
|
|
result_safe_again = _try_powerfx_eval("=Env.TEST_VAR")
|
|
assert result_safe_again == "=Env.TEST_VAR"
|
|
finally:
|
|
_safe_mode_context.reset(token)
|
|
|
|
|
|
class TestAgentManifest:
|
|
"""Tests for AgentManifest class."""
|
|
|
|
def test_agent_manifest_creation(self):
|
|
manifest = AgentManifest(name="my-agent-manifest", description="A test manifest")
|
|
assert manifest.name == "my-agent-manifest"
|
|
assert manifest.description == "A test manifest"
|
|
|
|
def test_agent_manifest_from_dict(self):
|
|
data = {
|
|
"name": "my-agent-manifest",
|
|
"description": "A test manifest",
|
|
}
|
|
manifest = AgentManifest.from_dict(data)
|
|
assert manifest.name == "my-agent-manifest"
|
|
|
|
def test_agent_manifest_with_resources(self):
|
|
data = {
|
|
"name": "my-agent-manifest",
|
|
"resources": [
|
|
{"name": "model1", "kind": "model", "id": "gpt-4"},
|
|
{
|
|
"name": "tool1",
|
|
"kind": "tool",
|
|
"id": "search-tool",
|
|
},
|
|
],
|
|
}
|
|
manifest = AgentManifest.from_dict(data)
|
|
assert manifest.name == "my-agent-manifest"
|
|
assert len(manifest.resources) == 2
|
|
# Resources are converted via Resource.from_dict based on their 'kind'
|
|
assert isinstance(manifest.resources[0], ModelResource)
|
|
assert isinstance(manifest.resources[1], ToolResource)
|
|
|
|
def test_agent_manifest_complete(self):
|
|
"""Test a complete agent manifest with all fields."""
|
|
data = {
|
|
"name": "complete-manifest",
|
|
"description": "A complete test manifest",
|
|
"template": {
|
|
"name": "assistant",
|
|
"kind": "Prompt",
|
|
"description": "A helpful assistant",
|
|
},
|
|
"resources": [
|
|
{"name": "model1", "kind": "model", "id": "gpt-4"},
|
|
],
|
|
}
|
|
manifest = AgentManifest.from_dict(data)
|
|
assert manifest.name == "complete-manifest"
|
|
assert isinstance(manifest.template, AgentDefinition)
|
|
assert len(manifest.resources) == 1
|
|
assert isinstance(manifest.resources[0], ModelResource)
|
|
|
|
def test_agent_manifest_with_dict_resources(self):
|
|
"""Test AgentManifest with dict format for resources (MAML YAML dict syntax)."""
|
|
data = {
|
|
"name": "manifest-with-dict-resources",
|
|
"description": "Test manifest with dict resources",
|
|
"resources": {
|
|
"gptModelDeployment": {"kind": "model", "id": "gpt-4o"},
|
|
"webSearchInstance": {"kind": "tool", "id": "web-search"},
|
|
"analyticsTool": {"kind": "tool", "id": "analytics"},
|
|
},
|
|
}
|
|
manifest = AgentManifest.from_dict(data)
|
|
assert manifest.name == "manifest-with-dict-resources"
|
|
assert len(manifest.resources) == 3
|
|
|
|
# Check that all resources were converted correctly
|
|
resource_names = {r.name for r in manifest.resources}
|
|
assert resource_names == {"gptModelDeployment", "webSearchInstance", "analyticsTool"}
|
|
|
|
# Check specific resource
|
|
gpt_resource = next(r for r in manifest.resources if r.name == "gptModelDeployment")
|
|
assert isinstance(gpt_resource, ModelResource)
|
|
assert gpt_resource.id == "gpt-4o"
|
|
|
|
web_resource = next(r for r in manifest.resources if r.name == "webSearchInstance")
|
|
assert isinstance(web_resource, ToolResource)
|
|
assert web_resource.id == "web-search"
|