Files
cleveragents-core/tests/unit/templates/test_base.py

738 lines
25 KiB
Python

"""
Unit tests for templates base module.
"""
import pytest
from cleveragents.templates.base import (
TemplateType,
TemplateParameter,
ComponentReference,
InstantiationContext,
BaseTemplate,
)
class TestTemplateType:
"""Test cases for TemplateType enum."""
def test_agent_type(self):
"""Test AGENT template type."""
assert TemplateType.AGENT.value == "agent"
def test_graph_type(self):
"""Test GRAPH template type."""
assert TemplateType.GRAPH.value == "graph"
def test_stream_type(self):
"""Test STREAM template type."""
assert TemplateType.STREAM.value == "stream"
class TestTemplateParameter:
"""Test cases for TemplateParameter class."""
def test_init_with_defaults(self):
"""Test TemplateParameter initialization with defaults."""
param = TemplateParameter(name="test_param")
assert param.name == "test_param"
assert param.default is None
assert param.type == "string"
assert param.values is None
assert param.description is None
assert param.required is False
def test_init_with_custom_values(self):
"""Test TemplateParameter initialization with custom values."""
param = TemplateParameter(
name="custom",
default="default_value",
type="enum",
values=["a", "b", "c"],
description="Test parameter",
required=True
)
assert param.name == "custom"
assert param.default == "default_value"
assert param.type == "enum"
assert param.values == ["a", "b", "c"]
assert param.description == "Test parameter"
assert param.required is True
def test_validate_required_missing(self):
"""Test validation fails for missing required parameter."""
param = TemplateParameter(name="required_param", required=True)
with pytest.raises(ValueError, match="Required parameter"):
param.validate(None)
def test_validate_with_default(self):
"""Test validation uses default when value is None."""
param = TemplateParameter(name="param", default="default_val")
result = param.validate(None)
assert result == "default_val"
def test_validate_string_type(self):
"""Test string type validation."""
param = TemplateParameter(name="str_param", type="string")
assert param.validate("hello") == "hello"
assert param.validate(123) == "123"
def test_validate_int_type(self):
"""Test int type validation."""
param = TemplateParameter(name="int_param", type="int")
assert param.validate(42) == 42
assert param.validate("100") == 100
def test_validate_float_type(self):
"""Test float type validation."""
param = TemplateParameter(name="float_param", type="float")
assert param.validate(3.14) == 3.14
assert param.validate("2.5") == 2.5
def test_validate_boolean_type_from_string(self):
"""Test boolean type validation from string."""
param = TemplateParameter(name="bool_param", type="boolean")
assert param.validate("true") is True
assert param.validate("yes") is True
assert param.validate("1") is True
assert param.validate("on") is True
assert param.validate("false") is False
assert param.validate("no") is False
def test_validate_boolean_type_from_bool(self):
"""Test boolean type validation from bool."""
param = TemplateParameter(name="bool_param", type="boolean")
assert param.validate(True) is True
assert param.validate(False) is False
assert param.validate(1) is True
assert param.validate(0) is False
def test_validate_enum_type_valid(self):
"""Test enum type validation with valid value."""
param = TemplateParameter(
name="enum_param",
type="enum",
values=["option1", "option2", "option3"]
)
assert param.validate("option1") == "option1"
assert param.validate("option2") == "option2"
def test_validate_enum_type_invalid(self):
"""Test enum type validation with invalid value."""
param = TemplateParameter(
name="enum_param",
type="enum",
values=["option1", "option2"]
)
with pytest.raises(ValueError, match="must be one of"):
param.validate("invalid_option")
def test_validate_list_type_from_list(self):
"""Test list type validation from list."""
param = TemplateParameter(name="list_param", type="list")
result = param.validate([1, 2, 3])
assert result == [1, 2, 3]
def test_validate_list_type_from_single_value(self):
"""Test list type validation converts single value to list."""
param = TemplateParameter(name="list_param", type="list")
result = param.validate("single")
assert result == ["single"]
def test_validate_dict_type(self):
"""Test dict type validation."""
param = TemplateParameter(name="dict_param", type="dict")
test_dict = {"key": "value"}
assert param.validate(test_dict) == test_dict
def test_validate_agent_ref_type(self):
"""Test agent_ref type validation."""
param = TemplateParameter(name="agent_param", type="agent_ref")
ref = {"type": "agent", "name": "test"}
assert param.validate(ref) == ref
def test_validate_component_ref_type(self):
"""Test component_ref type validation."""
param = TemplateParameter(name="comp_param", type="component_ref")
ref = {"type": "component", "name": "test"}
assert param.validate(ref) == ref
def test_validate_unknown_type(self):
"""Test validation with unknown type returns value as-is."""
param = TemplateParameter(name="unknown", type="unknown_type")
assert param.validate("value") == "value"
class TestComponentReference:
"""Test cases for ComponentReference class."""
def test_init(self):
"""Test ComponentReference initialization."""
ref = ComponentReference(ref_type="agent", ref_name="test_agent")
assert ref.ref_type == "agent"
assert ref.ref_name == "test_agent"
assert ref.ref_params == {}
def test_init_with_params(self):
"""Test ComponentReference initialization with params."""
params = {"param1": "value1"}
ref = ComponentReference(
ref_type="graph",
ref_name="test_graph",
ref_params=params
)
assert ref.ref_type == "graph"
assert ref.ref_name == "test_graph"
assert ref.ref_params == params
def test_resolve(self):
"""Test ComponentReference resolve method."""
ref = ComponentReference(ref_type="agent", ref_name="test")
context = InstantiationContext()
context.add_component("agent", "test", {"config": "value"})
resolved = ref.resolve(context)
assert resolved is not None
assert resolved["config"] == "value"
class TestInstantiationContext:
"""Test cases for InstantiationContext class."""
def test_init(self):
"""Test InstantiationContext initialization."""
context = InstantiationContext()
assert context.parent is None
assert "agents" in context.components
assert "graphs" in context.components
assert "streams" in context.components
def test_init_with_parent(self):
"""Test InstantiationContext initialization with parent."""
parent = InstantiationContext()
child = InstantiationContext(parent=parent)
assert child.parent is parent
def test_add_component_agent(self):
"""Test adding an agent component."""
context = InstantiationContext()
agent_config = {"type": "llm", "model": "gpt-4"}
context.add_component("agent", "test_agent", agent_config)
assert "test_agent" in context.components["agents"]
assert context.components["agents"]["test_agent"] == agent_config
def test_add_component_graph(self):
"""Test adding a graph component."""
context = InstantiationContext()
graph_config = {"nodes": {}, "edges": []}
context.add_component("graph", "test_graph", graph_config)
assert "test_graph" in context.components["graphs"]
def test_add_component_stream(self):
"""Test adding a stream component."""
context = InstantiationContext()
stream_config = {"type": "hot"}
context.add_component("stream", "test_stream", stream_config)
assert "test_stream" in context.components["streams"]
def test_resolve_reference_local(self):
"""Test resolving reference from local context."""
context = InstantiationContext()
agent_config = {"type": "llm"}
context.add_component("agent", "local_agent", agent_config)
ref = ComponentReference(ref_type="agent", ref_name="local_agent")
resolved = context.resolve_reference(ref)
assert resolved == agent_config
def test_resolve_reference_parent(self):
"""Test resolving reference from parent context."""
parent = InstantiationContext()
parent.add_component("agent", "parent_agent", {"type": "tool"})
child = InstantiationContext(parent=parent)
ref = ComponentReference(ref_type="agent", ref_name="parent_agent")
resolved = child.resolve_reference(ref)
assert resolved is not None
assert resolved["type"] == "tool"
def test_resolve_reference_not_found(self):
"""Test resolving reference that doesn't exist returns None."""
context = InstantiationContext()
ref = ComponentReference(ref_type="agent", ref_name="missing")
resolved = context.resolve_reference(ref)
assert resolved is None
def test_resolve_pending_success(self):
"""Test resolve_pending when references can be resolved."""
context = InstantiationContext()
# Add a pending reference
ref = ComponentReference(ref_type="agent", ref_name="deferred")
context.resolve_reference(ref) # Adds to pending
# Now add the component
context.add_component("agent", "deferred", {"type": "llm"})
# Should not raise
context.resolve_pending()
def test_resolve_pending_failure(self):
"""Test resolve_pending raises error for unresolvable references."""
context = InstantiationContext()
# Add a reference that can't be resolved
ref = ComponentReference(ref_type="agent", ref_name="missing")
context.resolve_reference(ref)
with pytest.raises(ValueError, match="Cannot resolve references"):
context.resolve_pending()
def test_get_all_components(self):
"""Test getting all components returns deep copy."""
context = InstantiationContext()
context.add_component("agent", "agent1", {"type": "llm"})
context.add_component("graph", "graph1", {"nodes": {}})
all_components = context.get_all_components()
assert "agents" in all_components
assert "graphs" in all_components
assert "agent1" in all_components["agents"]
assert "graph1" in all_components["graphs"]
# Verify it's a deep copy
all_components["agents"]["agent1"]["modified"] = True
assert "modified" not in context.components["agents"]["agent1"]
class ConcreteTemplate(BaseTemplate):
"""Concrete implementation of BaseTemplate for testing."""
def instantiate(self, params, registry, context):
"""Simple instantiation for testing."""
filled = self.validate_params(params)
return self._apply_template_vars(self.definition, filled)
class TestBaseTemplate:
"""Test cases for BaseTemplate class."""
def test_init(self):
"""Test BaseTemplate initialization."""
definition = {"type": "llm", "parameters": {}}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
assert template.name == "test"
assert template.template_type == TemplateType.AGENT
assert template.definition == definition
def test_parse_parameters_dict_format(self):
"""Test parsing parameters in dict format."""
definition = {
"parameters": {
"param1": {"type": "string", "default": "value1"},
"param2": {"type": "int", "required": True}
}
}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
assert "param1" in template.parameters
assert "param2" in template.parameters
assert template.parameters["param1"].type == "string"
assert template.parameters["param2"].required is True
def test_parse_parameters_dict_simple_values(self):
"""Test parsing parameters with simple default values."""
definition = {
"parameters": {
"param1": "default_value",
"param2": 42
}
}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
assert template.parameters["param1"].default == "default_value"
assert template.parameters["param2"].default == 42
def test_parse_parameters_list_format(self):
"""Test parsing parameters in list format."""
definition = {
"parameters": [
{"name": "param1", "type": "string"},
{"name": "param2", "type": "int"}
]
}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
assert "param1" in template.parameters
assert "param2" in template.parameters
def test_parse_parameters_list_simple_names(self):
"""Test parsing parameters as simple list of names."""
definition = {
"parameters": ["param1", "param2", "param3"]
}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
assert "param1" in template.parameters
assert "param2" in template.parameters
assert "param3" in template.parameters
def test_parse_parameters_no_parameters(self):
"""Test parsing when no parameters defined."""
definition = {"type": "llm"}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
assert template.parameters == {}
def test_validate_params(self):
"""Test parameter validation."""
definition = {
"parameters": {
"param1": {"type": "string", "default": "default"},
"param2": {"type": "int", "required": True}
}
}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template.validate_params({"param2": 42})
assert result["param1"] == "default"
assert result["param2"] == 42
def test_validate_params_extra_params(self):
"""Test that extra params are included."""
definition = {"parameters": {"param1": "default"}}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template.validate_params({"param1": "value", "extra": "included"})
assert result["extra"] == "included"
def test_apply_template_vars_string_simple(self):
"""Test applying template vars to simple string."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("{{ name }}", {"name": "test_value"})
assert result == "test_value"
def test_apply_template_vars_string_no_template(self):
"""Test string without template syntax."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("plain string", {})
assert result == "plain string"
def test_apply_template_vars_boolean_true(self):
"""Test rendering boolean true."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("{{ flag }}", {"flag": "true"})
assert result is True
def test_apply_template_vars_boolean_false(self):
"""Test rendering boolean false."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("{{ flag }}", {"flag": "false"})
assert result is False
def test_apply_template_vars_int(self):
"""Test rendering integer."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("{{ count }}", {"count": 42})
assert result == 42
def test_apply_template_vars_float(self):
"""Test rendering float."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("{{ value }}", {"value": 3.14})
assert result == 3.14
def test_apply_template_vars_list(self):
"""Test rendering list."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars("{{ items }}", {"items": "[1, 2, 3]"})
assert result == [1, 2, 3]
def test_apply_template_vars_dict(self):
"""Test rendering dict."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
result = template._apply_template_vars('{{ data }}', {"data": '{"key": "value"}'})
assert result == {"key": "value"}
def test_apply_template_vars_dict_recursive(self):
"""Test applying template vars recursively to dict."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = {
"name": "{{ agent_name }}",
"nested": {
"value": "{{ nested_value }}"
}
}
params = {"agent_name": "test_agent", "nested_value": "nested"}
result = template._apply_template_vars(config, params)
assert result["name"] == "test_agent"
assert result["nested"]["value"] == "nested"
def test_apply_template_vars_list_items(self):
"""Test applying template vars to list items."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = ["{{ item1 }}", "{{ item2 }}", "static"]
params = {"item1": "value1", "item2": "value2"}
result = template._apply_template_vars(config, params)
assert result == ["value1", "value2", "static"]
def test_apply_template_vars_conditional_true(self):
"""Test conditional template block when condition is true."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = {
"{% if include %}": "included: true"
}
params = {"include": True}
result = template._apply_template_vars(config, params)
assert result is not None
def test_apply_template_vars_conditional_false(self):
"""Test conditional template block when condition is false."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = {
"{% if include %}": "included: true"
}
params = {"include": False}
result = template._apply_template_vars(config, params)
assert result is None
def test_apply_template_vars_dict_key_templating(self):
"""Test applying templates to dict keys."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = {
"{{ key_name }}": "value"
}
params = {"key_name": "dynamic_key"}
result = template._apply_template_vars(config, params)
assert "dynamic_key" in result
assert result["dynamic_key"] == "value"
def test_apply_template_vars_filters_none_values(self):
"""Test that None values are filtered from dicts."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = {
"keep": "value",
"remove": None
}
params = {}
result = template._apply_template_vars(config, params)
assert "keep" in result
# None values should be filtered out
assert "remove" not in result or result.get("remove") is None
def test_apply_template_vars_rendering_error(self):
"""Test handling of template rendering with undefined variables."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
# Template with undefined variable renders as empty string in Jinja2
config = "{{ undefined_var }}"
params = {}
result = template._apply_template_vars(config, params)
# Jinja2 renders undefined vars as empty string by default
assert result == ""
def test_merge_params(self):
"""Test merging parameter dictionaries."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
base = {"param1": "base1", "param2": "base2"}
override = {"param2": "override2", "param3": "override3"}
result = template._merge_params(base, override)
assert result["param1"] == "base1"
assert result["param2"] == "override2"
assert result["param3"] == "override3"
def test_merge_params_with_template_vars(self):
"""Test merging params applies template vars to overrides."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
base = {"name": "test"}
override = {"model": "{{ name }}_model"}
result = template._merge_params(base, override)
assert result["model"] == "test_model"
def test_merge_params_preserves_base(self):
"""Test that merge doesn't modify base params."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
base = {"param": "value"}
override = {"param": "new_value"}
result = template._merge_params(base, override)
assert base["param"] == "value" # Original unchanged
assert result["param"] == "new_value"
def test_apply_template_vars_json_parse_error(self):
"""Test handling JSON parse error for dict-like strings."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
# String that looks like dict but has really invalid JSON
config = "{{ data }}"
params = {"data": "{not json at all}"}
result = template._apply_template_vars(config, params)
# Should return as string when JSON parsing fails
assert isinstance(result, str)
def test_apply_template_vars_list_with_none_items(self):
"""Test that None items are filtered from lists."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
config = ["item1", None, "item2"]
params = {}
result = template._apply_template_vars(config, params)
# None should be filtered out
assert None not in result
assert "item1" in result
assert "item2" in result
def test_apply_template_vars_conditional_yaml_parse_error(self):
"""Test conditional block with YAML parse error."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
# Conditional that renders invalid YAML
config = {
"{% if enabled %}": ": invalid yaml :::"
}
params = {"enabled": True}
result = template._apply_template_vars(config, params)
# Should handle the error gracefully
assert result is not None
def test_apply_template_vars_jinja_exception(self):
"""Test handling Jinja2 rendering exception."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
# Template that will cause an exception (division by zero)
config = "{{ 1 / 0 }}"
params = {}
result = template._apply_template_vars(config, params)
# Should return original config on exception
assert result == config
def test_apply_template_vars_number_parsing_error(self):
"""Test number parsing fallback to string."""
definition = {}
template = ConcreteTemplate("test", TemplateType.AGENT, definition)
# Template that renders to non-numeric string
config = "{{ value }}"
params = {"value": "not_a_number"}
result = template._apply_template_vars(config, params)
# Should return as string when number parsing fails
assert result == "not_a_number"
if __name__ == "__main__":
pytest.main([__file__, "-v"])