"""Step definitions for yaml_engine_direct_coverage.feature.""" from __future__ import annotations import tempfile from pathlib import Path from typing import Any import yaml from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from cleveragents.actor.yaml_template_engine import YAMLTemplateEngine class _HasScore: def __init__(self, score: float) -> None: self.score = score class _NoScore: pass @when("I construct a new YAMLTemplateEngine for direct testing") @given("I construct a new YAMLTemplateEngine for direct testing") def step_construct(context: Context) -> None: context.direct_engine = YAMLTemplateEngine() @then("the direct engine should have yaml indent sum selectattr filters") def step_check_filters(context: Context) -> None: for name in ("yaml", "indent", "sum", "selectattr"): assert name in context.direct_engine.env.filters, f"Missing filter: {name}" @then("the direct engine globals should include builtins") def step_check_globals(context: Context) -> None: g = context.direct_engine.env.globals for name in ("range", "abs", "round", "len", "min", "max", "sum"): assert name in g, f"Missing global: {name}" @given("I write a temp YAML file with mapping content") def step_write_temp_mapping(context: Context) -> None: with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False, encoding="utf-8" ) as tmp: tmp.write("app:\n name: TestApp\n version: 1\n") tmp.flush() context.direct_temp_path = Path(tmp.name) @when("I call load_file on the direct engine without context") def step_load_file_no_ctx(context: Context) -> None: context.direct_result = context.direct_engine.load_file(context.direct_temp_path) @then("the direct load_file result should be a dict with app key") def step_assert_load_file(context: Context) -> None: r = context.direct_result assert isinstance(r, dict) assert "app" in r @when("I call load_string with plain mapping YAML on direct engine") def step_load_plain_mapping(context: Context) -> None: context.direct_result = context.direct_engine.load_string("server:\n port: 8080\n") @then("the direct result should have key server") def step_assert_server(context: Context) -> None: assert "server" in context.direct_result @when("I call load_string with plain list YAML on direct engine") def step_load_plain_list(context: Context) -> None: context.direct_error = None try: context.direct_engine.load_string("- a\n- b\n") except ValueError as exc: context.direct_error = exc @then("a direct ValueError mentioning dict should be raised") def step_assert_direct_valueerror(context: Context) -> None: assert context.direct_error is not None, "Expected ValueError" assert "dict" in str(context.direct_error).lower() @when("I call load_string with template and context on direct engine") def step_load_with_context(context: Context) -> None: yaml_str = 'greeting: "{{ message }}"\ncount: {{ num }}' ctx: dict[str, Any] = {"message": "hello", "num": 42} context.direct_result = context.direct_engine.load_string(yaml_str, context=ctx) @then("the direct rendered result should have substituted values") def step_assert_substituted(context: Context) -> None: assert context.direct_result["greeting"] == "hello" assert context.direct_result["count"] == 42 @when("I call load_string with template but no context on direct engine") def step_load_no_context(context: Context) -> None: yaml_str = 'item:\n name: "{{ name }}"\n' context.direct_error = None try: context.direct_result = context.direct_engine.load_string(yaml_str) except (yaml.YAMLError, ValueError) as exc: context.direct_error = exc context.direct_result = None @then("the direct deferred result should be a dict or raise YAMLError") def step_assert_deferred(context: Context) -> None: if context.direct_result is not None: assert isinstance(context.direct_result, dict) @when("I call preprocess on content with a for loop") def step_preprocess(context: Context) -> None: content = "items:\n {% for x in list %}\n - {{ x }}\n {% endfor %}\n" context.direct_result = context.direct_engine._preprocess_for_rendering(content) @then("the direct preprocessed result should contain indent hint") def step_assert_hint(context: Context) -> None: assert "{# indent:" in context.direct_result @when("I call postprocess on content with blank lines before unindented") def step_postprocess(context: Context) -> None: content = "first: value\n\nsecond: value\n" context.direct_result = context.direct_engine._postprocess_rendered_yaml(content) @then("blank lines before unindented content should be gone") def step_assert_no_blanks(context: Context) -> None: lines = context.direct_result.split("\n") for i, line in enumerate(lines): if line.strip() == "" and i + 1 < len(lines): next_line = lines[i + 1] if next_line and not next_line.startswith(" "): raise AssertionError("Blank line before unindented line not removed") @when("I call fix_common_yaml_issues on multi-colon content") def step_fix_colons(context: Context) -> None: content = "config: key1: val1 key2: val2" context.direct_result = context.direct_engine._fix_common_yaml_issues(content) @then("the fixed content should have multiple lines") def step_assert_multiline(context: Context) -> None: assert len(context.direct_result.split("\n")) > 1 @when("I call analyze_yaml_structure on mixed content") def step_analyze(context: Context) -> None: content = ( "# comment\nroot:\n value: static\n" " {{ inline_var }}\n {% for x in items %}\n" " - name: {{ x }}\n {% endfor %}\n" ) context.direct_result = context.direct_engine._analyze_yaml_structure(content) @then("template_blocks and inline_templates should be populated") def step_assert_blocks(context: Context) -> None: assert len(context.direct_result["template_blocks"]) > 0 assert len(context.direct_result["inline_templates"]) > 0 @then("hierarchy should have root key") def step_assert_root(context: Context) -> None: keys = [e["key"] for e in context.direct_result["hierarchy"]] assert "root" in keys @when("I call the static yaml_filter with a dict") def step_yaml_filter(context: Context) -> None: context.direct_result = YAMLTemplateEngine._yaml_filter({"key": "value"}) @then("the yaml_filter result should contain key colon") def step_assert_yaml(context: Context) -> None: assert "key:" in context.direct_result @when("I call the static indent_filter with {n:d} spaces") def step_indent_filter(context: Context, n: int) -> None: context.direct_result = YAMLTemplateEngine._indent_filter("line1\nline2", n) context.direct_indent = n @then("each direct line should start with {n:d} spaces") def step_assert_indent(context: Context, n: int) -> None: for line in context.direct_result.split("\n"): assert line.startswith(" " * n), f"Not indented: {line!r}" @when("I call the static sum_filter on a number list") def step_sum_filter(context: Context) -> None: context.direct_result = YAMLTemplateEngine._sum_filter([1, 2, 3, 4]) @then("the direct sum should be {n:d}") def step_assert_sum(context: Context, n: int) -> None: assert context.direct_result == n @when("I call the static selectattr_filter for score") def step_selectattr(context: Context) -> None: items = [_HasScore(0.9), _NoScore(), _HasScore(0.5)] context.direct_result = YAMLTemplateEngine._selectattr_filter(items, "score", 0) @then("items with score return their value and others return 0") def step_assert_selectattr(context: Context) -> None: assert context.direct_result[0] == 0.9 assert context.direct_result[1] == 0 assert context.direct_result[2] == 0.5 @when("I call the static create_render_context with nested data") def step_render_context(context: Context) -> None: ctx: dict[str, Any] = { "value": "explicit", "context": {"shared": "nested_val"}, } context.direct_result = YAMLTemplateEngine._create_render_context(ctx) @then("the direct context should have both nested and explicit keys") def step_assert_ctx(context: Context) -> None: assert context.direct_result["value"] == "explicit" assert context.direct_result["context"]["shared"] == "nested_val" @when("I call render_and_parse with colon-heavy value") def step_render_colons(context: Context) -> None: ctx: dict[str, Any] = {"val": "key1: v1 key2: v2"} context.direct_error = None context.direct_result = None try: context.direct_result = context.direct_engine._render_and_parse( "config: {{ val }}", ctx ) except (yaml.YAMLError, ValueError) as exc: context.direct_error = exc @then("the direct engine should attempt fix or raise") def step_assert_fix(context: Context) -> None: assert context.direct_result is not None or context.direct_error is not None @when("I call render_and_parse that produces a list") def step_render_list(context: Context) -> None: ctx: dict[str, Any] = {"items": ["a", "b"]} context.direct_error = None try: context.direct_engine._render_and_parse( "{% for item in items %}\n- {{ item }}\n{% endfor %}", ctx ) except ValueError as exc: context.direct_error = exc @when("I call simple_template_extraction with invalid YAML") def step_simple_invalid(context: Context) -> None: context.direct_error = None try: context.direct_engine._simple_template_extraction("key: {{ bad\n") except yaml.YAMLError as exc: context.direct_error = exc @then("a direct YAML error should be raised") def step_assert_yaml_error(context: Context) -> None: assert context.direct_error is not None, "Expected YAMLError" @when("I call simple_template_extraction with list YAML") def step_simple_list(context: Context) -> None: context.direct_error = None try: context.direct_engine._simple_template_extraction("- a\n- b\n") except ValueError as exc: context.direct_error = exc @given("I write a temp YAML file with template content") def step_write_template_file(context: Context) -> None: with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", delete=False, encoding="utf-8" ) as tmp: tmp.write('agent:\n name: "{{ name }}"\n role: static\n') tmp.flush() context.direct_template_path = Path(tmp.name) @when("I call load_file with template context on direct engine") def step_load_file_with_ctx(context: Context) -> None: ctx: dict[str, Any] = {"name": "TestAgent"} context.direct_result = context.direct_engine.load_file( context.direct_template_path, context=ctx ) @then("the direct file result should contain rendered values") def step_assert_file_rendered(context: Context) -> None: assert isinstance(context.direct_result, dict) assert context.direct_result["agent"]["name"] == "TestAgent"