forked from HAL9000/cleveragents-core
916 lines
29 KiB
Python
916 lines
29 KiB
Python
"""
|
|
Comprehensive unit tests for reactive config_parser module.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from cleveragents.core.exceptions import ConfigurationError
|
|
from cleveragents.reactive.config_parser import (
|
|
AgentConfig,
|
|
HybridPipelineConfig,
|
|
LangGraphConfig,
|
|
ReactiveConfig,
|
|
ReactiveConfigParser,
|
|
)
|
|
from cleveragents.reactive.route import RouteConfig, RouteType
|
|
from cleveragents.reactive.stream_router import StreamType
|
|
|
|
|
|
@pytest.fixture
|
|
def parser():
|
|
"""Fixture for ReactiveConfigParser instance."""
|
|
return ReactiveConfigParser()
|
|
|
|
|
|
class TestAgentConfig:
|
|
"""Test cases for AgentConfig dataclass."""
|
|
|
|
def test_agent_config_creation(self):
|
|
"""Test AgentConfig creation."""
|
|
config = AgentConfig(name="test_agent", type="llm")
|
|
|
|
assert config.name == "test_agent"
|
|
assert config.type == "llm"
|
|
assert config.config == {}
|
|
|
|
def test_agent_config_with_config_dict(self):
|
|
"""Test AgentConfig with config dictionary."""
|
|
config = AgentConfig(
|
|
name="test_agent",
|
|
type="llm",
|
|
config={"model": "gpt-4", "temperature": 0.7}
|
|
)
|
|
|
|
assert config.config["model"] == "gpt-4"
|
|
assert config.config["temperature"] == 0.7
|
|
|
|
|
|
class TestLangGraphConfig:
|
|
"""Test cases for LangGraphConfig dataclass."""
|
|
|
|
def test_langgraph_config_defaults(self):
|
|
"""Test LangGraphConfig with defaults."""
|
|
config = LangGraphConfig(name="test_graph")
|
|
|
|
assert config.name == "test_graph"
|
|
assert config.nodes == {}
|
|
assert config.edges == []
|
|
assert config.entry_point == "start"
|
|
assert config.checkpointing is False
|
|
assert config.checkpoint_dir is None
|
|
assert config.enable_time_travel is False
|
|
assert config.parallel_execution is True
|
|
assert config.state_class is None
|
|
assert config.metadata == {}
|
|
assert config.template_config is None
|
|
|
|
def test_langgraph_config_full(self):
|
|
"""Test LangGraphConfig with all fields."""
|
|
config = LangGraphConfig(
|
|
name="test_graph",
|
|
nodes={"node1": {"type": "agent"}},
|
|
edges=[{"source": "start", "target": "node1"}],
|
|
entry_point="start",
|
|
checkpointing=True,
|
|
checkpoint_dir="/tmp",
|
|
enable_time_travel=True,
|
|
parallel_execution=False,
|
|
state_class="CustomState",
|
|
metadata={"key": "value"},
|
|
template_config={"template": "test"}
|
|
)
|
|
|
|
assert len(config.nodes) == 1
|
|
assert len(config.edges) == 1
|
|
assert config.checkpointing is True
|
|
assert config.checkpoint_dir == "/tmp"
|
|
|
|
|
|
class TestHybridPipelineConfig:
|
|
"""Test cases for HybridPipelineConfig dataclass."""
|
|
|
|
def test_hybrid_pipeline_config_defaults(self):
|
|
"""Test HybridPipelineConfig with defaults."""
|
|
config = HybridPipelineConfig(name="test_pipeline")
|
|
|
|
assert config.name == "test_pipeline"
|
|
assert config.stages == []
|
|
assert config.metadata == {}
|
|
|
|
def test_hybrid_pipeline_config_with_stages(self):
|
|
"""Test HybridPipelineConfig with stages."""
|
|
config = HybridPipelineConfig(
|
|
name="test_pipeline",
|
|
stages=[
|
|
{"type": "stream", "name": "input"},
|
|
{"type": "graph", "name": "processor"}
|
|
],
|
|
metadata={"version": "1.0"}
|
|
)
|
|
|
|
assert len(config.stages) == 2
|
|
assert config.metadata["version"] == "1.0"
|
|
|
|
|
|
class TestReactiveConfig:
|
|
"""Test cases for ReactiveConfig dataclass."""
|
|
|
|
def test_reactive_config_defaults(self):
|
|
"""Test ReactiveConfig with defaults."""
|
|
config = ReactiveConfig()
|
|
|
|
assert config.agents == {}
|
|
assert config.routes == {}
|
|
assert config.merges == []
|
|
assert config.splits == []
|
|
assert config.pipelines == {}
|
|
assert config.templates == {}
|
|
assert config.instances == {}
|
|
assert config.global_context == {}
|
|
assert config.template_engine == "JINJA2"
|
|
assert config.prompts == {}
|
|
|
|
|
|
class TestReactiveConfigParserInit:
|
|
"""Test cases for ReactiveConfigParser initialization."""
|
|
|
|
def test_parser_init(self, parser):
|
|
"""Test parser initialization."""
|
|
assert parser.logger is not None
|
|
assert isinstance(parser.logger, logging.Logger)
|
|
|
|
|
|
class TestParseFiles:
|
|
"""Test cases for parse_files method."""
|
|
|
|
def test_parse_files_empty_list(self, parser):
|
|
"""Test parsing empty file list."""
|
|
result = parser.parse_files([])
|
|
|
|
assert isinstance(result, ReactiveConfig)
|
|
|
|
def test_parse_files_single_file(self, parser):
|
|
"""Test parsing single file."""
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
yaml.dump({
|
|
"agents": {
|
|
"agent1": {
|
|
"type": "llm",
|
|
"config": {"model": "gpt-4"}
|
|
}
|
|
}
|
|
}, f)
|
|
f.flush()
|
|
filepath = Path(f.name)
|
|
|
|
try:
|
|
result = parser.parse_files([filepath])
|
|
|
|
assert "agent1" in result.agents
|
|
assert result.agents["agent1"].type == "llm"
|
|
finally:
|
|
os.unlink(filepath)
|
|
|
|
def test_parse_files_multiple_files(self, parser):
|
|
"""Test parsing multiple files."""
|
|
# Create first file
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f1:
|
|
yaml.dump({
|
|
"agents": {
|
|
"agent1": {"type": "llm"}
|
|
}
|
|
}, f1)
|
|
f1.flush()
|
|
filepath1 = Path(f1.name)
|
|
|
|
# Create second file
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f2:
|
|
yaml.dump({
|
|
"agents": {
|
|
"agent2": {"type": "tool"}
|
|
}
|
|
}, f2)
|
|
f2.flush()
|
|
filepath2 = Path(f2.name)
|
|
|
|
try:
|
|
result = parser.parse_files([filepath1, filepath2])
|
|
|
|
assert "agent1" in result.agents
|
|
assert "agent2" in result.agents
|
|
finally:
|
|
os.unlink(filepath1)
|
|
os.unlink(filepath2)
|
|
|
|
def test_parse_files_with_templates(self, parser):
|
|
"""Test parsing file with templates."""
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
yaml.dump({
|
|
"templates": {
|
|
"agents": {
|
|
"template1": {
|
|
"type": "llm",
|
|
"parameters": {"model": "gpt-4"}
|
|
}
|
|
}
|
|
}
|
|
}, f)
|
|
f.flush()
|
|
filepath = Path(f.name)
|
|
|
|
try:
|
|
result = parser.parse_files([filepath])
|
|
|
|
assert "agents" in result.templates
|
|
finally:
|
|
os.unlink(filepath)
|
|
|
|
def test_parse_files_with_jinja2_template_syntax(self, parser):
|
|
"""Test parsing file with Jinja2 template syntax ({{% or {{)."""
|
|
# Create a file without Jinja2 syntax first to establish baseline
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
yaml.dump({
|
|
"agents": {
|
|
"test_agent": {
|
|
"type": "llm",
|
|
"config": {"model": "gpt-4"}
|
|
}
|
|
}
|
|
}, f)
|
|
f.flush()
|
|
filepath1 = Path(f.name)
|
|
|
|
# Create a file WITH Jinja2 syntax
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
# Write a file with Jinja2 template syntax in comments (will be detected)
|
|
f.write("""
|
|
# This file uses Jinja2: {{ test }}
|
|
agents:
|
|
test_agent2:
|
|
type: llm
|
|
""")
|
|
f.flush()
|
|
filepath2 = Path(f.name)
|
|
|
|
try:
|
|
# Parse file without Jinja2 - should work normally
|
|
result1 = parser.parse_files([filepath1])
|
|
assert "test_agent" in result1.agents
|
|
|
|
# Parse file with Jinja2 syntax - should be handled by YAMLTemplateEngine
|
|
# The engine will use deferred rendering, so parsing should not fail
|
|
result2 = parser.parse_files([filepath2])
|
|
# Result might be empty or have special structure due to deferred rendering
|
|
assert isinstance(result2, ReactiveConfig)
|
|
finally:
|
|
os.unlink(filepath1)
|
|
os.unlink(filepath2)
|
|
|
|
|
|
class TestInterpolateEnvVars:
|
|
"""Test cases for _interpolate_env_vars method."""
|
|
|
|
def test_interpolate_env_vars_string(self, parser):
|
|
"""Test interpolating environment variables in string."""
|
|
os.environ["TEST_VAR"] = "test_value"
|
|
|
|
result = parser._interpolate_env_vars("${TEST_VAR}")
|
|
|
|
assert result == "test_value"
|
|
|
|
del os.environ["TEST_VAR"]
|
|
|
|
def test_interpolate_env_vars_dict(self, parser):
|
|
"""Test interpolating environment variables in dict."""
|
|
os.environ["TEST_KEY"] = "test_value"
|
|
|
|
config = {"key": "${TEST_KEY}"}
|
|
result = parser._interpolate_env_vars(config)
|
|
|
|
assert result["key"] == "test_value"
|
|
|
|
del os.environ["TEST_KEY"]
|
|
|
|
def test_interpolate_env_vars_list(self, parser):
|
|
"""Test interpolating environment variables in list."""
|
|
os.environ["TEST_ITEM"] = "item_value"
|
|
|
|
config = ["${TEST_ITEM}", "other"]
|
|
result = parser._interpolate_env_vars(config)
|
|
|
|
assert result[0] == "item_value"
|
|
assert result[1] == "other"
|
|
|
|
del os.environ["TEST_ITEM"]
|
|
|
|
def test_interpolate_env_vars_with_default(self, parser):
|
|
"""Test interpolating with default value."""
|
|
result = parser._interpolate_env_vars("${NONEXISTENT_VAR:-default_value}")
|
|
|
|
# The actual result includes the ":-" separator in the output
|
|
assert "default_value" in result
|
|
|
|
def test_interpolate_env_vars_no_match(self, parser):
|
|
"""Test interpolating when no environment variable pattern."""
|
|
result = parser._interpolate_env_vars("plain_string")
|
|
|
|
assert result == "plain_string"
|
|
|
|
def test_interpolate_env_vars_default_boolean_true(self, parser):
|
|
"""Test replace_env_var with default boolean value true."""
|
|
result = parser._interpolate_env_vars("${NONEXISTENT_VAR:-true}")
|
|
|
|
# Should convert to boolean string
|
|
assert "true" in str(result).lower()
|
|
|
|
def test_interpolate_env_vars_default_boolean_false(self, parser):
|
|
"""Test replace_env_var with default boolean value false."""
|
|
result = parser._interpolate_env_vars("${NONEXISTENT_VAR:-false}")
|
|
|
|
# Should convert to boolean string
|
|
assert "false" in str(result).lower()
|
|
|
|
def test_interpolate_env_vars_default_integer(self, parser):
|
|
"""Test replace_env_var with default integer value."""
|
|
result = parser._interpolate_env_vars("${NONEXISTENT_VAR:-42}")
|
|
|
|
# Should return the integer as string
|
|
assert "42" in str(result)
|
|
|
|
def test_interpolate_env_vars_default_float(self, parser):
|
|
"""Test replace_env_var with default float value."""
|
|
result = parser._interpolate_env_vars("${NONEXISTENT_VAR:-3.14}")
|
|
|
|
# Should return the float as string
|
|
assert "3.14" in str(result)
|
|
|
|
def test_interpolate_env_vars_missing_no_default_error(self, parser):
|
|
"""Test replace_env_var raises error when env var missing and no default."""
|
|
with pytest.raises(ConfigurationError, match="Environment variable.*is not set"):
|
|
parser._interpolate_env_vars("${NONEXISTENT_VAR_NO_DEFAULT}")
|
|
|
|
def test_interpolate_env_vars_converts_true_string(self, parser):
|
|
"""Test interpolation converts 'true' string to boolean."""
|
|
# Set and interpolate a 'true' value
|
|
os.environ["BOOL_VAR"] = "true"
|
|
|
|
result = parser._interpolate_env_vars("${BOOL_VAR}")
|
|
|
|
# Should convert to boolean True
|
|
assert result is True
|
|
|
|
del os.environ["BOOL_VAR"]
|
|
|
|
def test_interpolate_env_vars_converts_integer_string(self, parser):
|
|
"""Test interpolation converts integer string."""
|
|
# Set and interpolate an integer value
|
|
os.environ["INT_VAR"] = "123"
|
|
|
|
result = parser._interpolate_env_vars("${INT_VAR}")
|
|
|
|
# Should convert to integer
|
|
assert result == 123
|
|
assert isinstance(result, int)
|
|
|
|
del os.environ["INT_VAR"]
|
|
|
|
def test_interpolate_env_vars_converts_float_string(self, parser):
|
|
"""Test interpolation converts float string."""
|
|
# Set and interpolate a float value
|
|
os.environ["FLOAT_VAR"] = "45.67"
|
|
|
|
result = parser._interpolate_env_vars("${FLOAT_VAR}")
|
|
|
|
# Should convert to float
|
|
assert result == 45.67
|
|
assert isinstance(result, float)
|
|
|
|
del os.environ["FLOAT_VAR"]
|
|
|
|
|
|
class TestMergeConfigs:
|
|
"""Test cases for _merge_configs method."""
|
|
|
|
def test_merge_configs_simple(self, parser):
|
|
"""Test merging simple configs."""
|
|
base = {"key1": "value1"}
|
|
new = {"key2": "value2"}
|
|
|
|
parser._merge_configs(base, new)
|
|
|
|
assert base["key1"] == "value1"
|
|
assert base["key2"] == "value2"
|
|
|
|
def test_merge_configs_nested(self, parser):
|
|
"""Test merging nested configs."""
|
|
base = {"nested": {"key1": "value1"}}
|
|
new = {"nested": {"key2": "value2"}}
|
|
|
|
parser._merge_configs(base, new)
|
|
|
|
assert base["nested"]["key1"] == "value1"
|
|
assert base["nested"]["key2"] == "value2"
|
|
|
|
def test_merge_configs_overwrite(self, parser):
|
|
"""Test that new values overwrite old values."""
|
|
base = {"key": "old_value"}
|
|
new = {"key": "new_value"}
|
|
|
|
parser._merge_configs(base, new)
|
|
|
|
assert base["key"] == "new_value"
|
|
|
|
def test_merge_configs_list_append(self, parser):
|
|
"""Test merging lists."""
|
|
base = {"items": ["item1"]}
|
|
new = {"items": ["item2"]}
|
|
|
|
parser._merge_configs(base, new)
|
|
|
|
assert base["items"] == ["item1", "item2"]
|
|
|
|
def test_merge_configs_none_input(self, parser):
|
|
"""Test merging when new is None."""
|
|
base = {"key1": "value1", "key2": "value2"}
|
|
original_base = base.copy()
|
|
|
|
parser._merge_configs(base, None)
|
|
|
|
# Base should remain unchanged when new is None
|
|
assert base == original_base
|
|
|
|
|
|
class TestParseStreamRoute:
|
|
"""Test cases for _parse_stream_route method."""
|
|
|
|
def test_parse_stream_route_basic(self, parser):
|
|
"""Test parsing basic stream route."""
|
|
route_data = {
|
|
"type": "stream",
|
|
"stream_type": "cold",
|
|
"operators": []
|
|
}
|
|
|
|
result = parser._parse_stream_route("test_stream", route_data)
|
|
|
|
assert result.name == "test_stream"
|
|
assert result.type == RouteType.STREAM
|
|
assert result.stream_type == StreamType.COLD
|
|
|
|
def test_parse_stream_route_with_operators(self, parser):
|
|
"""Test parsing stream route with operators."""
|
|
route_data = {
|
|
"type": "stream",
|
|
"operators": [
|
|
{"type": "map", "function": "transform"}
|
|
]
|
|
}
|
|
|
|
result = parser._parse_stream_route("test_stream", route_data)
|
|
|
|
assert len(result.operators) == 1
|
|
assert result.operators[0]["type"] == "map"
|
|
|
|
def test_parse_stream_route_hot_stream(self, parser):
|
|
"""Test parsing hot stream."""
|
|
route_data = {
|
|
"type": "stream",
|
|
"stream_type": "hot",
|
|
"initial_value": "init"
|
|
}
|
|
|
|
result = parser._parse_stream_route("hot_stream", route_data)
|
|
|
|
assert result.stream_type == StreamType.HOT
|
|
assert result.initial_value == "init"
|
|
|
|
|
|
class TestParseGraphRoute:
|
|
"""Test cases for _parse_graph_route method."""
|
|
|
|
def test_parse_graph_route_basic(self, parser):
|
|
"""Test parsing basic graph route."""
|
|
route_data = {
|
|
"type": "graph",
|
|
"nodes": {
|
|
"node1": {"type": "agent"}
|
|
},
|
|
"edges": []
|
|
}
|
|
|
|
result = parser._parse_graph_route("test_graph", route_data)
|
|
|
|
assert result.name == "test_graph"
|
|
assert result.type == RouteType.GRAPH
|
|
assert "node1" in result.nodes
|
|
|
|
def test_parse_graph_route_with_edges(self, parser):
|
|
"""Test parsing graph route with edges."""
|
|
route_data = {
|
|
"type": "graph",
|
|
"nodes": {
|
|
"node1": {"type": "agent"},
|
|
"node2": {"type": "agent"}
|
|
},
|
|
"edges": [
|
|
{"source": "node1", "target": "node2"}
|
|
]
|
|
}
|
|
|
|
result = parser._parse_graph_route("test_graph", route_data)
|
|
|
|
assert len(result.edges) == 1
|
|
assert result.edges[0]["source"] == "node1"
|
|
|
|
def test_parse_graph_route_with_checkpointing(self, parser):
|
|
"""Test parsing graph route with checkpointing."""
|
|
route_data = {
|
|
"type": "graph",
|
|
"nodes": {"node1": {"type": "agent"}},
|
|
"checkpointing": True,
|
|
"checkpoint_dir": "/tmp/checkpoints"
|
|
}
|
|
|
|
result = parser._parse_graph_route("test_graph", route_data)
|
|
|
|
assert result.checkpointing is True
|
|
assert result.checkpoint_dir == "/tmp/checkpoints"
|
|
|
|
|
|
class TestParseBridgeRoute:
|
|
"""Test cases for _parse_bridge_route method."""
|
|
|
|
def test_parse_bridge_route(self, parser):
|
|
"""Test parsing bridge route."""
|
|
route_data = {
|
|
"type": "bridge",
|
|
"bridge": {
|
|
"upgrade_conditions": {"type": "complex"},
|
|
"downgrade_conditions": {"type": "simple"}
|
|
}
|
|
}
|
|
|
|
result = parser._parse_bridge_route("test_bridge", route_data)
|
|
|
|
assert result.name == "test_bridge"
|
|
assert result.type == RouteType.BRIDGE
|
|
assert result.bridge is not None
|
|
|
|
|
|
class TestBuildReactiveConfig:
|
|
"""Test cases for _build_reactive_config method."""
|
|
|
|
def test_build_reactive_config_empty(self, parser):
|
|
"""Test building config from empty dict."""
|
|
result = parser._build_reactive_config({})
|
|
|
|
assert isinstance(result, ReactiveConfig)
|
|
assert result.agents == {}
|
|
|
|
def test_build_reactive_config_with_agents(self, parser):
|
|
"""Test building config with agents."""
|
|
config = {
|
|
"agents": {
|
|
"agent1": {
|
|
"type": "llm",
|
|
"config": {"model": "gpt-4"}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = parser._build_reactive_config(config)
|
|
|
|
assert "agent1" in result.agents
|
|
assert result.agents["agent1"].type == "llm"
|
|
|
|
def test_build_reactive_config_with_routes(self, parser):
|
|
"""Test building config with routes."""
|
|
config = {
|
|
"routes": {
|
|
"stream1": {
|
|
"type": "stream",
|
|
"stream_type": "cold"
|
|
}
|
|
}
|
|
}
|
|
|
|
result = parser._build_reactive_config(config)
|
|
|
|
assert "stream1" in result.routes
|
|
|
|
def test_build_reactive_config_with_templates(self, parser):
|
|
"""Test building config with templates."""
|
|
config = {
|
|
"templates": {
|
|
"agents": {
|
|
"template1": {"type": "llm"}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = parser._build_reactive_config(config)
|
|
|
|
assert "agents" in result.templates
|
|
|
|
def test_build_reactive_config_with_prompts(self, parser):
|
|
"""Test building config with prompts."""
|
|
config = {
|
|
"prompts": {
|
|
"prompt1": "This is a test prompt"
|
|
}
|
|
}
|
|
|
|
result = parser._build_reactive_config(config)
|
|
|
|
assert "prompt1" in result.prompts
|
|
assert result.prompts["prompt1"] == "This is a test prompt"
|
|
|
|
def test_build_reactive_config_with_template_strings(self, parser):
|
|
"""Test building config where template_strings is not empty."""
|
|
config = {
|
|
"template_strings": {
|
|
"agents": {
|
|
"template1": "some_jinja2_template_string"
|
|
}
|
|
}
|
|
}
|
|
|
|
result = parser._build_reactive_config(config)
|
|
|
|
# template_strings should be merged into templates
|
|
assert "agents" in result.templates
|
|
assert "template1" in result.templates["agents"]
|
|
|
|
def test_build_reactive_config_with_agent_template(self, parser):
|
|
"""Test building config with template/agent_template in agent_data."""
|
|
config = {
|
|
"agents": {
|
|
"agent1": {
|
|
"template": "llm_template",
|
|
"parameters": {"model": "gpt-4"}
|
|
},
|
|
"agent2": {
|
|
"agent_template": "tool_template",
|
|
"parameters": {"tools": ["shell"]}
|
|
}
|
|
}
|
|
}
|
|
|
|
result = parser._build_reactive_config(config)
|
|
|
|
# Both agents should be of type "template_instance"
|
|
assert result.agents["agent1"].type == "template_instance"
|
|
assert result.agents["agent2"].type == "template_instance"
|
|
assert "template" in result.agents["agent1"].config
|
|
assert "agent_template" in result.agents["agent2"].config
|
|
|
|
|
|
class TestValidateConfig:
|
|
"""Test cases for _validate_config method."""
|
|
|
|
def test_validate_config_valid(self, parser):
|
|
"""Test validating valid config."""
|
|
config = ReactiveConfig()
|
|
|
|
# Should not raise
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_missing_agent(self, parser):
|
|
"""Test validation when route references missing agent."""
|
|
route = RouteConfig(
|
|
name="test_stream",
|
|
type=RouteType.STREAM,
|
|
agents=["nonexistent_agent"]
|
|
)
|
|
|
|
config = ReactiveConfig(
|
|
routes={"test_stream": route}
|
|
)
|
|
|
|
# Should raise ConfigurationError
|
|
with pytest.raises(ConfigurationError):
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_stream_route_type(self, parser):
|
|
"""Test validation with RouteType.STREAM in routes."""
|
|
# Create an agent first
|
|
agent = AgentConfig(name="test_agent", type="llm")
|
|
|
|
# Create a valid stream route
|
|
route = RouteConfig(
|
|
name="test_stream",
|
|
type=RouteType.STREAM,
|
|
agents=["test_agent"],
|
|
subscriptions=["__input__"],
|
|
publications=["__output__"]
|
|
)
|
|
|
|
config = ReactiveConfig(
|
|
agents={"test_agent": agent},
|
|
routes={"test_stream": route}
|
|
)
|
|
|
|
# Should not raise any errors
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_graph_route_type(self, parser):
|
|
"""Test validation with RouteType.GRAPH in routes."""
|
|
# Create an agent first
|
|
agent = AgentConfig(name="test_agent", type="llm")
|
|
|
|
# Create a valid graph route
|
|
route = RouteConfig(
|
|
name="test_graph",
|
|
type=RouteType.GRAPH,
|
|
nodes={
|
|
"node1": {"type": "agent", "agent": "test_agent"},
|
|
"node2": {"type": "agent"}
|
|
},
|
|
edges=[{"source": "node1", "target": "node2"}]
|
|
)
|
|
|
|
config = ReactiveConfig(
|
|
agents={"test_agent": agent},
|
|
routes={"test_graph": route}
|
|
)
|
|
|
|
# Should not raise any errors
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_merges(self, parser):
|
|
"""Test validation when config.merges has values."""
|
|
# Create routes for merge
|
|
route1 = RouteConfig(name="stream1", type=RouteType.STREAM)
|
|
route2 = RouteConfig(name="stream2", type=RouteType.STREAM)
|
|
target_route = RouteConfig(name="merged_stream", type=RouteType.STREAM)
|
|
|
|
config = ReactiveConfig(
|
|
routes={
|
|
"stream1": route1,
|
|
"stream2": route2,
|
|
"merged_stream": target_route
|
|
},
|
|
merges=[
|
|
{
|
|
"sources": ["stream1", "stream2"],
|
|
"target": "merged_stream"
|
|
}
|
|
]
|
|
)
|
|
|
|
# Should not raise any errors
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_splits(self, parser):
|
|
"""Test validation when config.splits has values."""
|
|
# Create routes for split
|
|
source_route = RouteConfig(name="source_stream", type=RouteType.STREAM)
|
|
route1 = RouteConfig(name="stream1", type=RouteType.STREAM)
|
|
route2 = RouteConfig(name="stream2", type=RouteType.STREAM)
|
|
|
|
config = ReactiveConfig(
|
|
routes={
|
|
"source_stream": source_route,
|
|
"stream1": route1,
|
|
"stream2": route2
|
|
},
|
|
splits=[
|
|
{
|
|
"source": "source_stream",
|
|
"targets": {
|
|
"condition1": "stream1",
|
|
"condition2": "stream2"
|
|
}
|
|
}
|
|
]
|
|
)
|
|
|
|
# Should not raise any errors
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_pipeline_stage_graph(self, parser):
|
|
"""Test validation where pipeline has stage_type='graph'."""
|
|
# Create a graph route
|
|
graph_route = RouteConfig(
|
|
name="test_graph",
|
|
type=RouteType.GRAPH,
|
|
nodes={"node1": {"type": "agent"}},
|
|
edges=[]
|
|
)
|
|
|
|
config = ReactiveConfig(
|
|
routes={"test_graph": graph_route},
|
|
pipelines={
|
|
"pipeline1": HybridPipelineConfig(
|
|
name="pipeline1",
|
|
stages=[
|
|
{
|
|
"type": "graph",
|
|
"config": {"name": "test_graph"}
|
|
}
|
|
]
|
|
)
|
|
}
|
|
)
|
|
|
|
# Should not raise any errors (might log a warning if graph not found)
|
|
parser._validate_config(config)
|
|
|
|
def test_validate_config_with_pipeline_stage_stream(self, parser):
|
|
"""Test validation where pipeline has stage_type='stream'."""
|
|
# Create a stream route
|
|
stream_route = RouteConfig(
|
|
name="test_stream",
|
|
type=RouteType.STREAM
|
|
)
|
|
|
|
config = ReactiveConfig(
|
|
routes={"test_stream": stream_route},
|
|
pipelines={
|
|
"pipeline1": HybridPipelineConfig(
|
|
name="pipeline1",
|
|
stages=[
|
|
{
|
|
"type": "stream",
|
|
"name": "test_stream"
|
|
}
|
|
]
|
|
)
|
|
}
|
|
)
|
|
|
|
# Should not raise any errors (might log a warning if stream already exists)
|
|
parser._validate_config(config)
|
|
|
|
|
|
class TestComplexScenarios:
|
|
"""Test cases for complex parsing scenarios."""
|
|
|
|
def test_parse_complete_config_file(self, parser):
|
|
"""Test parsing complete config file."""
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
yaml.dump({
|
|
"agents": {
|
|
"agent1": {
|
|
"type": "llm",
|
|
"config": {"model": "gpt-4"}
|
|
}
|
|
},
|
|
"routes": {
|
|
"stream1": {
|
|
"type": "stream",
|
|
"stream_type": "cold",
|
|
"operators": []
|
|
}
|
|
},
|
|
"prompts": {
|
|
"prompt1": "Test prompt"
|
|
}
|
|
}, f)
|
|
f.flush()
|
|
filepath = Path(f.name)
|
|
|
|
try:
|
|
result = parser.parse_files([filepath])
|
|
|
|
assert "agent1" in result.agents
|
|
assert "stream1" in result.routes
|
|
assert "prompt1" in result.prompts
|
|
finally:
|
|
os.unlink(filepath)
|
|
|
|
def test_parse_config_with_env_interpolation(self, parser):
|
|
"""Test parsing config with environment variable interpolation."""
|
|
os.environ["MODEL_NAME"] = "gpt-4"
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
yaml.dump({
|
|
"agents": {
|
|
"agent1": {
|
|
"type": "llm",
|
|
"config": {"model": "${MODEL_NAME}"}
|
|
}
|
|
}
|
|
}, f)
|
|
f.flush()
|
|
filepath = Path(f.name)
|
|
|
|
try:
|
|
result = parser.parse_files([filepath])
|
|
|
|
assert result.agents["agent1"].config["model"] == "gpt-4"
|
|
finally:
|
|
os.unlink(filepath)
|
|
del os.environ["MODEL_NAME"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|
|
|