forked from cleveragents/cleveragents-core
c6157745e7
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.
833 lines
36 KiB
Python
833 lines
36 KiB
Python
"""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"
|
|
)
|