forked from cleveragents/cleveragents-core
test(actor): add step definitions for actor examples validation
Create features/steps/actor_examples_steps.py with step implementations: - Given steps for actor patterns (strategist/executor/reviewer/validator) - When steps for schema validation from string and file - Then steps for field validation and pattern verification - Graph/route validation (nodes, edges, conditionals, subgraphs) - Error assertions and documentation completeness checks All steps use unique patterns to avoid AmbiguousStep conflicts.
This commit is contained in:
@@ -0,0 +1,832 @@
|
||||
"""Step definitions for actor YAML examples validation.
|
||||
|
||||
Tests for features/actor_examples.feature — validates that all actor examples
|
||||
are correct, loadable, and demonstrate the expected patterns (strategist,
|
||||
executor, reviewer, tool-only, validation, graphs, hierarchical).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from behave import given, then, when # type: ignore[import-untyped]
|
||||
from behave.runner import Context # type: ignore[import-untyped]
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.actor.schema import ActorConfigSchema, NodeType
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Given Steps - Background
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given("the CleverAgents actor schema validation is available")
|
||||
def step_given_schema_available(context: Context) -> None:
|
||||
"""Verify that ActorConfigSchema is importable and ready."""
|
||||
context.schema_available = ActorConfigSchema is not None
|
||||
assert context.schema_available, "ActorConfigSchema should be available"
|
||||
|
||||
|
||||
@given("the actor YAML file {file_path:QuotedString}")
|
||||
def step_given_actor_yaml_file_examples(context: Context, file_path: str) -> None:
|
||||
"""Load actor YAML file path for validation."""
|
||||
# Strip quotes if any
|
||||
file_path = file_path.strip('"').strip("'")
|
||||
context.actor_yaml_file = file_path
|
||||
context.actor_yaml_string = None # Clear any previous string
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Given Steps - Actor Pattern YAMLs
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given("an actor YAML string representing a strategist actor")
|
||||
def step_given_strategist_actor(context: Context) -> None:
|
||||
"""Provide strategist actor YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string representing an executor actor")
|
||||
def step_given_executor_actor(context: Context) -> None:
|
||||
"""Provide executor actor YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string representing a reviewer actor")
|
||||
def step_given_reviewer_actor(context: Context) -> None:
|
||||
"""Provide reviewer actor YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with tool-only configuration")
|
||||
def step_given_tool_only_actor(context: Context) -> None:
|
||||
"""Provide tool-only actor YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with git tools")
|
||||
def step_given_git_tools_actor(context: Context) -> None:
|
||||
"""Provide git tools actor YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string representing a validator actor")
|
||||
def step_given_validator_actor(context: Context) -> None:
|
||||
"""Provide validator actor YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with linear graph workflow")
|
||||
def step_given_linear_graph_workflow(context: Context) -> None:
|
||||
"""Provide linear graph workflow YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with conditional routing")
|
||||
def step_given_conditional_routing_examples(context: Context) -> None:
|
||||
"""Provide conditional routing YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with tool nodes")
|
||||
def step_given_tool_nodes(context: Context) -> None:
|
||||
"""Provide YAML with tool nodes from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with subgraph")
|
||||
def step_given_subgraph_examples(context: Context) -> None:
|
||||
"""Provide YAML with subgraph from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with retry logic")
|
||||
def step_given_retry_logic(context: Context) -> None:
|
||||
"""Provide YAML with retry logic from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with multi-level hierarchy")
|
||||
def step_given_multi_level_hierarchy(context: Context) -> None:
|
||||
"""Provide multi-level hierarchy YAML from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Given Steps - Invalid Examples
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given("an actor YAML string missing required name field")
|
||||
def step_given_missing_name(context: Context) -> None:
|
||||
"""Provide YAML missing name field from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with invalid name")
|
||||
def step_given_invalid_name(context: Context) -> None:
|
||||
"""Provide YAML with invalid name from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string for LLM without model")
|
||||
def step_given_llm_without_model(context: Context) -> None:
|
||||
"""Provide LLM YAML without model from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string for TOOL without tools")
|
||||
def step_given_tool_without_tools(context: Context) -> None:
|
||||
"""Provide TOOL YAML without tools from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string for GRAPH without route")
|
||||
def step_given_graph_without_route(context: Context) -> None:
|
||||
"""Provide GRAPH YAML without route from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
@given("an actor YAML string with cyclic graph")
|
||||
def step_given_cyclic_graph(context: Context) -> None:
|
||||
"""Provide YAML with cyclic graph from docstring."""
|
||||
context.actor_yaml_string = context.text
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Given Steps - Directory and Documentation
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given("the examples/actors directory")
|
||||
def step_given_examples_directory(context: Context) -> None:
|
||||
"""Set context to examples/actors directory."""
|
||||
context.examples_dir = Path("examples/actors")
|
||||
assert context.examples_dir.exists(), (
|
||||
f"Directory {context.examples_dir} should exist"
|
||||
)
|
||||
|
||||
|
||||
@given("the documentation file {doc_path:QuotedString}")
|
||||
def step_given_documentation_file(context: Context, doc_path: str) -> None:
|
||||
"""Load documentation file for verification."""
|
||||
doc_path = doc_path.strip('"').strip("'")
|
||||
context.doc_file = Path(doc_path)
|
||||
assert context.doc_file.exists(), (
|
||||
f"Documentation file {context.doc_file} should exist"
|
||||
)
|
||||
context.doc_content = context.doc_file.read_text()
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# When Steps
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@when("I validate the actor schema")
|
||||
def step_when_validate_actor_schema_examples(context: Context) -> None:
|
||||
"""Validate actor schema from YAML string."""
|
||||
try:
|
||||
context.actor_config = ActorConfigSchema.from_yaml(context.actor_yaml_string)
|
||||
context.validation_error = None
|
||||
except ValidationError as e:
|
||||
context.actor_config = None
|
||||
context.validation_error = e
|
||||
except Exception as e:
|
||||
context.actor_config = None
|
||||
context.validation_error = e
|
||||
|
||||
|
||||
@when("I validate the actor schema from file")
|
||||
def step_when_validate_from_file_examples(context: Context) -> None:
|
||||
"""Validate actor schema from YAML file."""
|
||||
try:
|
||||
context.actor_config = ActorConfigSchema.from_yaml_file(context.actor_yaml_file)
|
||||
context.validation_error = None
|
||||
except ValidationError as e:
|
||||
context.actor_config = None
|
||||
context.validation_error = e
|
||||
except Exception as e:
|
||||
context.actor_config = None
|
||||
context.validation_error = e
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Validation Success/Failure
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor schema validation should succeed")
|
||||
def step_then_validation_succeeds_examples(context: Context) -> None:
|
||||
"""Assert validation succeeded."""
|
||||
if context.validation_error:
|
||||
raise AssertionError(
|
||||
f"Expected validation to succeed but got error: {context.validation_error}"
|
||||
)
|
||||
assert context.actor_config is not None, "Actor config should be populated"
|
||||
|
||||
|
||||
@then("the actor schema validation should fail")
|
||||
def step_then_validation_fails_examples(context: Context) -> None:
|
||||
"""Assert validation failed."""
|
||||
assert context.validation_error is not None, "Expected validation to fail"
|
||||
assert context.actor_config is None, "Actor config should be None on failure"
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Basic Field Checks
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor config name should be {expected_name:QuotedString}")
|
||||
def step_then_name_is_examples(context: Context, expected_name: str) -> None:
|
||||
"""Verify actor name matches expected value."""
|
||||
expected_name = expected_name.strip('"').strip("'")
|
||||
assert context.actor_config.name == expected_name, (
|
||||
f"Expected name '{expected_name}', got '{context.actor_config.name}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor config type should be {expected_type:QuotedString}")
|
||||
def step_then_type_is_examples(context: Context, expected_type: str) -> None:
|
||||
"""Verify actor type matches expected value."""
|
||||
expected_type = expected_type.strip('"').strip("'")
|
||||
actual_type = (
|
||||
context.actor_config.type.value
|
||||
if hasattr(context.actor_config.type, "value")
|
||||
else str(context.actor_config.type)
|
||||
)
|
||||
assert actual_type == expected_type, (
|
||||
f"Expected type '{expected_type}', got '{actual_type}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor config model should be {expected_model:QuotedString}")
|
||||
def step_then_model_is_examples(context: Context, expected_model: str) -> None:
|
||||
"""Verify actor model matches expected value."""
|
||||
expected_model = expected_model.strip('"').strip("'")
|
||||
assert context.actor_config.model == expected_model, (
|
||||
f"Expected model '{expected_model}', got '{context.actor_config.model}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor config description should contain {expected_text:QuotedString}")
|
||||
def step_then_description_contains_examples(
|
||||
context: Context, expected_text: str
|
||||
) -> None:
|
||||
"""Verify description contains expected text."""
|
||||
expected_text = expected_text.strip('"').strip("'")
|
||||
assert expected_text.lower() in context.actor_config.description.lower(), (
|
||||
f"Expected description to contain '{expected_text}', "
|
||||
f"got '{context.actor_config.description}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor config system_prompt should contain {expected_text:QuotedString}")
|
||||
def step_then_system_prompt_contains_examples(
|
||||
context: Context, expected_text: str
|
||||
) -> None:
|
||||
"""Verify system prompt contains expected text."""
|
||||
expected_text = expected_text.strip('"').strip("'")
|
||||
assert context.actor_config.system_prompt is not None, (
|
||||
"System prompt should not be None"
|
||||
)
|
||||
assert expected_text.lower() in context.actor_config.system_prompt.lower(), (
|
||||
f"Expected system_prompt to contain '{expected_text}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor config context_view should be {expected_view:QuotedString}")
|
||||
def step_then_context_view_is_examples(context: Context, expected_view: str) -> None:
|
||||
"""Verify context_view matches expected value."""
|
||||
expected_view = expected_view.strip('"').strip("'")
|
||||
actual_view = (
|
||||
context.actor_config.context_view.value
|
||||
if hasattr(context.actor_config.context_view, "value")
|
||||
else str(context.actor_config.context_view)
|
||||
)
|
||||
assert actual_view == expected_view, (
|
||||
f"Expected context_view '{expected_view}', got '{actual_view}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor config name should match pattern {pattern:QuotedString}")
|
||||
def step_then_name_matches_pattern_examples(context: Context, pattern: str) -> None:
|
||||
"""Verify actor name matches regex pattern."""
|
||||
import re
|
||||
|
||||
pattern = pattern.strip('"').strip("'")
|
||||
assert re.match(pattern, context.actor_config.name), (
|
||||
f"Name '{context.actor_config.name}' does not match pattern '{pattern}'"
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Memory Configuration
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor memory enabled should be {expected_bool}")
|
||||
def step_then_memory_enabled_examples(context: Context, expected_bool: str) -> None:
|
||||
"""Verify memory enabled flag."""
|
||||
expected = expected_bool.lower() == "true"
|
||||
assert context.actor_config.memory is not None, "Memory config should not be None"
|
||||
assert context.actor_config.memory.enabled == expected, (
|
||||
f"Expected memory.enabled={expected}, got {context.actor_config.memory.enabled}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor memory max_messages should be {expected:d}")
|
||||
def step_then_memory_max_messages_examples(context: Context, expected: int) -> None:
|
||||
"""Verify memory max_messages."""
|
||||
assert context.actor_config.memory is not None, "Memory config should not be None"
|
||||
assert context.actor_config.memory.max_messages == expected, (
|
||||
f"Expected memory.max_messages={expected}, "
|
||||
f"got {context.actor_config.memory.max_messages}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor memory max_tokens should be {expected:d}")
|
||||
def step_then_memory_max_tokens_examples(context: Context, expected: int) -> None:
|
||||
"""Verify memory max_tokens."""
|
||||
assert context.actor_config.memory is not None, "Memory config should not be None"
|
||||
assert context.actor_config.memory.max_tokens == expected, (
|
||||
f"Expected memory.max_tokens={expected}, got {context.actor_config.memory.max_tokens}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor memory max_messages should be at least {minimum:d}")
|
||||
def step_then_memory_max_messages_at_least_examples(
|
||||
context: Context, minimum: int
|
||||
) -> None:
|
||||
"""Verify memory max_messages is at least the specified minimum."""
|
||||
assert context.actor_config.memory is not None, "Memory config should not be None"
|
||||
assert context.actor_config.memory.max_messages >= minimum, (
|
||||
f"Expected memory.max_messages >= {minimum}, "
|
||||
f"got {context.actor_config.memory.max_messages}"
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Context Configuration
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor context should include {count:d} files")
|
||||
def step_then_context_includes_files_examples(context: Context, count: int) -> None:
|
||||
"""Verify context includes specified number of files."""
|
||||
assert context.actor_config.context is not None, "Context config should not be None"
|
||||
assert len(context.actor_config.context.include_files) == count, (
|
||||
f"Expected {count} include_files, "
|
||||
f"got {len(context.actor_config.context.include_files)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor context should include {count:d} directory")
|
||||
def step_then_context_includes_dirs_examples(context: Context, count: int) -> None:
|
||||
"""Verify context includes specified number of directories."""
|
||||
assert context.actor_config.context is not None, "Context config should not be None"
|
||||
assert len(context.actor_config.context.include_dirs) == count, (
|
||||
f"Expected {count} include_dirs, got {len(context.actor_config.context.include_dirs)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor context max_context_tokens should be {expected:d}")
|
||||
def step_then_context_max_tokens_examples(context: Context, expected: int) -> None:
|
||||
"""Verify context max_context_tokens."""
|
||||
assert context.actor_config.context is not None, "Context config should not be None"
|
||||
assert context.actor_config.context.max_context_tokens == expected, (
|
||||
f"Expected context.max_context_tokens={expected}, "
|
||||
f"got {context.actor_config.context.max_context_tokens}"
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Tools
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor config should have {count:d} tools")
|
||||
def step_then_has_exact_tool_count_examples(context: Context, count: int) -> None:
|
||||
"""Verify actor has exact number of tools."""
|
||||
tools = context.actor_config.tools or []
|
||||
assert len(tools) == count, f"Expected {count} tools, got {len(tools)}"
|
||||
|
||||
|
||||
@then("the actor config should have at least {minimum:d} tool")
|
||||
@then("the actor config should have at least {minimum:d} tools")
|
||||
def step_then_has_minimum_tools_examples(context: Context, minimum: int) -> None:
|
||||
"""Verify actor has at least minimum number of tools."""
|
||||
tools = context.actor_config.tools or []
|
||||
assert len(tools) >= minimum, f"Expected at least {minimum} tools, got {len(tools)}"
|
||||
|
||||
|
||||
@then("the actor config should have read-only tools")
|
||||
def step_then_has_read_only_tools_examples(context: Context) -> None:
|
||||
"""Verify actor has read-only tools (files/read, list, etc)."""
|
||||
tools = context.actor_config.tools or []
|
||||
assert len(tools) > 0, "Actor should have tools"
|
||||
# Check that tools include read operations
|
||||
tool_names = [
|
||||
str(tool) if isinstance(tool, str) else tool.get("name", "") for tool in tools
|
||||
]
|
||||
has_read_tools = any(
|
||||
"read" in name.lower() or "list" in name.lower() for name in tool_names
|
||||
)
|
||||
assert has_read_tools, "Actor should have read-only tools"
|
||||
|
||||
|
||||
@then("the actor config should have write-capable tools")
|
||||
def step_then_has_write_tools_examples(context: Context) -> None:
|
||||
"""Verify actor has write-capable tools."""
|
||||
tools = context.actor_config.tools or []
|
||||
assert len(tools) > 0, "Actor should have tools"
|
||||
tool_names = [
|
||||
str(tool) if isinstance(tool, str) else tool.get("name", "") for tool in tools
|
||||
]
|
||||
has_write_tools = any(
|
||||
"write" in name.lower() or "create" in name.lower() or "delete" in name.lower()
|
||||
for name in tool_names
|
||||
)
|
||||
assert has_write_tools, "Actor should have write-capable tools"
|
||||
|
||||
|
||||
@then("the actor config should have linting tools")
|
||||
def step_then_has_linting_tools_examples(context: Context) -> None:
|
||||
"""Verify actor has linting tools."""
|
||||
tools = context.actor_config.tools or []
|
||||
assert len(tools) > 0, "Actor should have tools"
|
||||
tool_names = [
|
||||
str(tool) if isinstance(tool, str) else tool.get("name", "") for tool in tools
|
||||
]
|
||||
has_linting = any(
|
||||
"lint" in name.lower() or "ruff" in name.lower() for name in tool_names
|
||||
)
|
||||
assert has_linting, "Actor should have linting tools"
|
||||
|
||||
|
||||
@then("the actor config should have validation tools")
|
||||
def step_then_has_validation_tools_examples(context: Context) -> None:
|
||||
"""Verify actor has validation tools."""
|
||||
tools = context.actor_config.tools or []
|
||||
assert len(tools) > 0, "Actor should have tools"
|
||||
tool_names = [
|
||||
str(tool) if isinstance(tool, str) else tool.get("name", "") for tool in tools
|
||||
]
|
||||
has_validation = any(
|
||||
"lint" in name.lower() or "test" in name.lower() or "security" in name.lower()
|
||||
for name in tool_names
|
||||
)
|
||||
assert has_validation, "Actor should have validation tools"
|
||||
|
||||
|
||||
@then("the actor config should not have model field")
|
||||
def step_then_no_model_field_examples(context: Context) -> None:
|
||||
"""Verify actor does not have model field."""
|
||||
assert context.actor_config.model is None, "TOOL actor should not have model field"
|
||||
|
||||
|
||||
@then("the actor config should not have system_prompt field")
|
||||
def step_then_no_system_prompt_field_examples(context: Context) -> None:
|
||||
"""Verify actor does not have system_prompt field."""
|
||||
assert context.actor_config.system_prompt is None, (
|
||||
"TOOL actor should not have system_prompt field"
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Graph/Route Configuration
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor route should have at least {minimum:d} nodes")
|
||||
def step_then_route_has_minimum_nodes_examples(context: Context, minimum: int) -> None:
|
||||
"""Verify route has at least minimum number of nodes."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert len(context.actor_config.route.nodes) >= minimum, (
|
||||
f"Expected at least {minimum} nodes, got {len(context.actor_config.route.nodes)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have at least {minimum:d} edges")
|
||||
def step_then_route_has_minimum_edges_examples(context: Context, minimum: int) -> None:
|
||||
"""Verify route has at least minimum number of edges."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert len(context.actor_config.route.edges) >= minimum, (
|
||||
f"Expected at least {minimum} edges, got {len(context.actor_config.route.edges)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have an entry_node")
|
||||
def step_then_route_has_entry_node_examples(context: Context) -> None:
|
||||
"""Verify route has entry_node defined."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert context.actor_config.route.entry_node is not None, (
|
||||
"entry_node should be defined"
|
||||
)
|
||||
assert len(context.actor_config.route.entry_node) > 0, (
|
||||
"entry_node should not be empty"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have exit_nodes")
|
||||
def step_then_route_has_exit_nodes_examples(context: Context) -> None:
|
||||
"""Verify route has exit_nodes defined."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert context.actor_config.route.exit_nodes is not None, (
|
||||
"exit_nodes should be defined"
|
||||
)
|
||||
assert len(context.actor_config.route.exit_nodes) > 0, (
|
||||
"exit_nodes should not be empty"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route entry_node should be {expected_node:QuotedString}")
|
||||
def step_then_entry_node_is_examples(context: Context, expected_node: str) -> None:
|
||||
"""Verify entry_node matches expected value."""
|
||||
expected_node = expected_node.strip('"').strip("'")
|
||||
assert context.actor_config.route.entry_node == expected_node, (
|
||||
f"Expected entry_node '{expected_node}', got '{context.actor_config.route.entry_node}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have {count:d} exit node")
|
||||
@then("the actor route should have {count:d} exit nodes")
|
||||
def step_then_has_exit_node_count_examples(context: Context, count: int) -> None:
|
||||
"""Verify route has specified number of exit nodes."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert len(context.actor_config.route.exit_nodes) == count, (
|
||||
f"Expected {count} exit nodes, got {len(context.actor_config.route.exit_nodes)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have {count:d} nodes")
|
||||
def step_then_route_has_exact_nodes_examples(context: Context, count: int) -> None:
|
||||
"""Verify route has exact number of nodes."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert len(context.actor_config.route.nodes) == count, (
|
||||
f"Expected {count} nodes, got {len(context.actor_config.route.nodes)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have conditional node")
|
||||
def step_then_has_conditional_node_examples(context: Context) -> None:
|
||||
"""Verify route has at least one conditional node."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
conditional_nodes = [
|
||||
node
|
||||
for node in context.actor_config.route.nodes
|
||||
if node.type == NodeType.CONDITIONAL
|
||||
]
|
||||
assert len(conditional_nodes) > 0, "Route should have at least one conditional node"
|
||||
|
||||
|
||||
@then("the actor route should have multiple exit nodes")
|
||||
def step_then_has_multiple_exit_nodes_examples(context: Context) -> None:
|
||||
"""Verify route has multiple exit nodes."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
assert len(context.actor_config.route.exit_nodes) > 1, (
|
||||
f"Expected multiple exit nodes, got {len(context.actor_config.route.exit_nodes)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should have tool node")
|
||||
def step_then_has_tool_node_examples(context: Context) -> None:
|
||||
"""Verify route has at least one tool node."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
tool_nodes = [
|
||||
node for node in context.actor_config.route.nodes if node.type == NodeType.TOOL
|
||||
]
|
||||
assert len(tool_nodes) > 0, "Route should have at least one tool node"
|
||||
|
||||
|
||||
@then("the actor route should have agent nodes")
|
||||
def step_then_has_agent_nodes_examples(context: Context) -> None:
|
||||
"""Verify route has agent nodes."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
agent_nodes = [
|
||||
node for node in context.actor_config.route.nodes if node.type == NodeType.AGENT
|
||||
]
|
||||
assert len(agent_nodes) > 0, "Route should have at least one agent node"
|
||||
|
||||
|
||||
@then("the actor route should have subgraph nodes")
|
||||
def step_then_has_subgraph_nodes_examples(context: Context) -> None:
|
||||
"""Verify route has subgraph nodes."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
subgraph_nodes = [
|
||||
node
|
||||
for node in context.actor_config.route.nodes
|
||||
if node.type == NodeType.SUBGRAPH
|
||||
]
|
||||
assert len(subgraph_nodes) > 0, "Route should have at least one subgraph node"
|
||||
|
||||
|
||||
@then("the actor route should have parallel execution config")
|
||||
def step_then_has_parallel_config_examples(context: Context) -> None:
|
||||
"""Verify route has parallel execution configuration."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
# Check if any subgraph node has parallel config
|
||||
subgraph_nodes = [
|
||||
node
|
||||
for node in context.actor_config.route.nodes
|
||||
if node.type == NodeType.SUBGRAPH
|
||||
]
|
||||
has_parallel = any(
|
||||
node.config and node.config.get("parallel") is True for node in subgraph_nodes
|
||||
)
|
||||
assert has_parallel, "Route should have parallel execution configuration"
|
||||
|
||||
|
||||
@then("the actor route should have retry loop")
|
||||
def step_then_has_retry_loop_examples(context: Context) -> None:
|
||||
"""Verify route has retry loop structure."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
# Check for conditional nodes with retry logic
|
||||
conditional_nodes = [
|
||||
node
|
||||
for node in context.actor_config.route.nodes
|
||||
if node.type == NodeType.CONDITIONAL
|
||||
]
|
||||
assert len(conditional_nodes) > 0, "Route should have conditional nodes for retry"
|
||||
# Check for edges forming a loop
|
||||
node_ids = {node.id for node in context.actor_config.route.nodes}
|
||||
has_loop = False
|
||||
for edge in context.actor_config.route.edges:
|
||||
if edge.from_node in node_ids and edge.to_node in node_ids:
|
||||
# Simple check: if we can go back to an earlier node
|
||||
from_idx = next(
|
||||
(
|
||||
i
|
||||
for i, n in enumerate(context.actor_config.route.nodes)
|
||||
if n.id == edge.from_node
|
||||
),
|
||||
-1,
|
||||
)
|
||||
to_idx = next(
|
||||
(
|
||||
i
|
||||
for i, n in enumerate(context.actor_config.route.nodes)
|
||||
if n.id == edge.to_node
|
||||
),
|
||||
-1,
|
||||
)
|
||||
if to_idx < from_idx:
|
||||
has_loop = True
|
||||
break
|
||||
assert has_loop, "Route should have retry loop (backward edge)"
|
||||
|
||||
|
||||
@then("the actor route should have escalation path")
|
||||
def step_then_has_escalation_path_examples(context: Context) -> None:
|
||||
"""Verify route has escalation path."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
# Check for tool nodes with escalation or notification in config
|
||||
tool_nodes = [
|
||||
node for node in context.actor_config.route.nodes if node.type == NodeType.TOOL
|
||||
]
|
||||
has_escalation = any(
|
||||
"escalat" in str(node.config).lower() or "alert" in str(node.config).lower()
|
||||
for node in tool_nodes
|
||||
)
|
||||
assert has_escalation, "Route should have escalation path"
|
||||
|
||||
|
||||
@then("the actor route should have multiple subgraph levels")
|
||||
def step_then_has_multiple_subgraph_levels_examples(context: Context) -> None:
|
||||
"""Verify route has multiple levels of subgraphs."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
subgraph_nodes = [
|
||||
node
|
||||
for node in context.actor_config.route.nodes
|
||||
if node.type == NodeType.SUBGRAPH
|
||||
]
|
||||
assert len(subgraph_nodes) >= 2, (
|
||||
f"Expected at least 2 subgraph nodes for multiple levels, "
|
||||
f"got {len(subgraph_nodes)}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor route should support parallel execution")
|
||||
def step_then_supports_parallel_execution_examples(context: Context) -> None:
|
||||
"""Verify route supports parallel execution."""
|
||||
assert context.actor_config.route is not None, "Route should not be None"
|
||||
subgraph_nodes = [
|
||||
node
|
||||
for node in context.actor_config.route.nodes
|
||||
if node.type == NodeType.SUBGRAPH and node.config
|
||||
]
|
||||
has_parallel_config = any(
|
||||
node.config.get("parallel") is True or "max_parallel" in node.config
|
||||
for node in subgraph_nodes
|
||||
)
|
||||
assert has_parallel_config, "Route should support parallel execution"
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Environment Variables
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the actor config should have environment variables")
|
||||
def step_then_has_env_vars_examples(context: Context) -> None:
|
||||
"""Verify actor has environment variables defined."""
|
||||
assert context.actor_config.env_vars is not None, "env_vars should not be None"
|
||||
assert len(context.actor_config.env_vars) > 0, "env_vars should not be empty"
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Error Messages
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the error message should contain {expected_text:QuotedString}")
|
||||
def step_then_error_contains_examples(context: Context, expected_text: str) -> None:
|
||||
"""Verify error message contains expected text."""
|
||||
expected_text = expected_text.strip('"').strip("'")
|
||||
assert context.validation_error is not None, "Should have validation error"
|
||||
error_str = str(context.validation_error).lower()
|
||||
assert expected_text.lower() in error_str, (
|
||||
f"Expected error to contain '{expected_text}', got: {context.validation_error}"
|
||||
)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# Then Steps - Example Files and Documentation
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("there should be exactly {count:d} example YAML files")
|
||||
def step_then_example_count_examples(context: Context, count: int) -> None:
|
||||
"""Verify exact number of example YAML files."""
|
||||
yaml_files = list(context.examples_dir.glob("*.yaml"))
|
||||
assert len(yaml_files) == count, (
|
||||
f"Expected {count} YAML files, found {len(yaml_files)}: {[f.name for f in yaml_files]}"
|
||||
)
|
||||
|
||||
|
||||
@then("the example files should include {filename:QuotedString}")
|
||||
def step_then_example_includes_file_examples(context: Context, filename: str) -> None:
|
||||
"""Verify specific example file exists."""
|
||||
filename = filename.strip('"').strip("'")
|
||||
file_path = context.examples_dir / filename
|
||||
assert file_path.exists(), (
|
||||
f"Example file {filename} should exist in {context.examples_dir}"
|
||||
)
|
||||
|
||||
|
||||
@then("the documentation should reference {filename:QuotedString}")
|
||||
def step_then_doc_references_file_examples(context: Context, filename: str) -> None:
|
||||
"""Verify documentation references specific file."""
|
||||
filename = filename.strip('"').strip("'")
|
||||
assert filename in context.doc_content, f"Documentation should reference {filename}"
|
||||
|
||||
|
||||
@then("the documentation should have examples for strategist actors")
|
||||
def step_then_doc_has_strategist_examples(context: Context) -> None:
|
||||
"""Verify documentation has strategist examples."""
|
||||
assert "strategist" in context.doc_content.lower(), (
|
||||
"Documentation should have strategist actor examples"
|
||||
)
|
||||
|
||||
|
||||
@then("the documentation should have examples for executor actors")
|
||||
def step_then_doc_has_executor_examples(context: Context) -> None:
|
||||
"""Verify documentation has executor examples."""
|
||||
assert "executor" in context.doc_content.lower(), (
|
||||
"Documentation should have executor actor examples"
|
||||
)
|
||||
|
||||
|
||||
@then("the documentation should have examples for reviewer actors")
|
||||
def step_then_doc_has_reviewer_examples(context: Context) -> None:
|
||||
"""Verify documentation has reviewer examples."""
|
||||
assert "reviewer" in context.doc_content.lower(), (
|
||||
"Documentation should have reviewer actor examples"
|
||||
)
|
||||
|
||||
|
||||
@then("the documentation should have examples for tool-only actors")
|
||||
def step_then_doc_has_tool_only_examples(context: Context) -> None:
|
||||
"""Verify documentation has tool-only examples."""
|
||||
assert (
|
||||
"tool-only" in context.doc_content.lower()
|
||||
or "tool only" in context.doc_content.lower()
|
||||
), "Documentation should have tool-only actor examples"
|
||||
|
||||
|
||||
@then("the documentation should have examples for validation actors")
|
||||
def step_then_doc_has_validation_examples(context: Context) -> None:
|
||||
"""Verify documentation has validation examples."""
|
||||
assert "validation" in context.doc_content.lower(), (
|
||||
"Documentation should have validation actor examples"
|
||||
)
|
||||
|
||||
|
||||
@then("the documentation should have examples for hierarchical graphs")
|
||||
def step_then_doc_has_hierarchical_examples(context: Context) -> None:
|
||||
"""Verify documentation has hierarchical graph examples."""
|
||||
assert "hierarchical" in context.doc_content.lower(), (
|
||||
"Documentation should have hierarchical graph examples"
|
||||
)
|
||||
Reference in New Issue
Block a user