diff --git a/features/steps/actor_examples_steps.py b/features/steps/actor_examples_steps.py new file mode 100644 index 00000000..80d6306c --- /dev/null +++ b/features/steps/actor_examples_steps.py @@ -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" + )