Files
temp/features/steps/yaml_template_engine_coverage_boost_steps.py
freemo a808c395f9 test(coverage): add Behave BDD tests to improve unit test coverage across 53 source modules
Add 53 new .feature files and corresponding step definition files targeting
uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts
in 7 pre-existing step files by disambiguating step text.

New tests cover: ACP clients/facade, actor CLI/config, application container,
ACMS service/strategies, async worker, automation profile CLI, autonomy
guardrail, bridge, change model, config CLI/service, context service,
cross-plan correction, database models, decision service, decomposition
clustering/service, discovery handler, langchain chat provider, langgraph
nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/
preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI,
provider registry, reactive application/route, repositories, resolver handler,
resource registry service, resume model, retry patterns, sandbox protocol,
server CLI, skill CLI/service, skills registry, subplan execution/service,
system CLI, UKO loader, UoW, and YAML template engine.

Closes #645
2026-03-09 13:01:58 -04:00

231 lines
8.6 KiB
Python

"""Behave steps targeting uncovered lines in yaml_template_engine.py.
Uncovered lines targeted:
101-105 - fallback parse path after _fix_common_yaml_issues (dict and non-dict)
134-136 - _postprocess_rendered_yaml value_words branch
138-142 - dead-code branch (documented; closest reachable path exercised)
172 - _fix_common_yaml_issues single/zero-colon line passthrough
236-237 - _sum_filter ValueError for non-summable input
"""
from __future__ import annotations
from typing import Any
from unittest import mock
import yaml
from behave import given, then, when
# Keep a reference to the module for mock.patch.object
import cleveragents.actor.yaml_template_engine as _mod
from cleveragents.actor.yaml_template_engine import YAMLTemplateEngine
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a fresh YAML template engine instance")
def step_fresh_engine(context: Any) -> None:
context.engine = YAMLTemplateEngine()
context.result = None
context.error = None
# ---------------------------------------------------------------------------
# Lines 101-105 - fallback returns dict
# ---------------------------------------------------------------------------
@given("a YAML template with Jinja2 that renders valid content")
def step_template_for_fallback(context: Any) -> None:
# Must contain {{ so load_string takes the template path
context.yaml_template = "result: {{ val }}"
@given("a context that produces parseable YAML on fix retry")
def step_context_for_fallback(context: Any) -> None:
context.template_ctx = {"val": "hello", "context": {}}
@when("I render and parse with a forced first-parse YAMLError returning a dict")
def step_render_fallback_dict(context: Any) -> None:
"""Mock yaml.safe_load so the first call raises YAMLError and the second
returns a dict - exercising lines 100-101-105."""
side_effects = [
yaml.YAMLError("simulated first-parse failure"),
{"result": "recovered-value"},
]
with mock.patch.object(_mod.yaml, "safe_load", side_effect=side_effects):
context.result = context.engine._render_and_parse(
context.yaml_template, context.template_ctx
)
@then("the fallback parse should return a dict result")
def step_assert_fallback_dict(context: Any) -> None:
assert isinstance(context.result, dict), (
f"Expected dict, got {type(context.result)}"
)
assert context.result == {"result": "recovered-value"}
@when("I render and parse with a forced first-parse YAMLError returning a list")
def step_render_fallback_list(context: Any) -> None:
"""Mock yaml.safe_load so the first call raises YAMLError and the second
returns a list - exercising lines 100-101-102-103-104."""
side_effects = [
yaml.YAMLError("simulated first-parse failure"),
["not", "a", "dict"],
]
try:
with mock.patch.object(_mod.yaml, "safe_load", side_effect=side_effects):
context.result = context.engine._render_and_parse(
context.yaml_template, context.template_ctx
)
context.error = None
except Exception as exc:
context.result = None
context.error = exc
@then("a ValueError should be raised from the fallback path")
def step_assert_fallback_valueerror(context: Any) -> None:
assert isinstance(context.error, ValueError), (
f"Expected ValueError, got {type(context.error)}: {context.error}"
)
assert "dict" in str(context.error)
# The ValueError should chain from the original YAMLError
assert context.error.__cause__ is not None
# ---------------------------------------------------------------------------
# Lines 134-136 - _postprocess_rendered_yaml value_words branch
# ---------------------------------------------------------------------------
@given("rendered YAML with a single-colon line whose value has multiple words")
def step_rendered_single_colon_spaced_value(context: Any) -> None:
# line.count(":") == 1, value_part has spaces, ":" not in value_part
# This reaches line 134 (value_words = ...) and line 135-136 (any() → False)
context.postprocess_input = "config: alpha beta gamma delta"
@when("I postprocess the YAML content")
def step_postprocess_content(context: Any) -> None:
context.postprocess_output = context.engine._postprocess_rendered_yaml(
context.postprocess_input
)
@then("the value_words branch should be entered and evaluated")
def step_assert_value_words_entered(context: Any) -> None:
# Because value has no colons in subsequent words, the inner condition is
# False and the line passes through unchanged.
assert context.postprocess_output.strip() == "config: alpha beta gamma delta"
# ---------------------------------------------------------------------------
# Lines 138-142 - dead code (closest reachable path)
# ---------------------------------------------------------------------------
@given('rendered YAML content "{content}" with spaces in value')
def step_rendered_content_with_spaces(context: Any, content: str) -> None:
context.postprocess_input = content
@then("the line should pass through unchanged because value words lack colons")
def step_assert_unchanged_no_colon_words(context: Any) -> None:
# The guard ":" not in value_part on line 133 prevents any word from having
# a colon, so the inner any() on line 135-136 is always False.
# The line passes to fixed_lines.append(line) on line 143.
assert context.postprocess_input.strip() in context.postprocess_output
# ---------------------------------------------------------------------------
# Line 172 - _fix_common_yaml_issues passthrough for <=1 colon lines
# ---------------------------------------------------------------------------
@given('YAML content with a single-colon line "{content}"')
def step_yaml_single_colon_line(context: Any, content: str) -> None:
context.fix_input = content
@when("I apply fix common YAML issues")
def step_apply_fix_common(context: Any) -> None:
context.fix_output = context.engine._fix_common_yaml_issues(context.fix_input)
@then("the single-colon line should be preserved verbatim")
def step_assert_single_colon_preserved(context: Any) -> None:
assert context.fix_output == context.fix_input
@given('YAML content with a zero-colon line "{content}"')
def step_yaml_zero_colon_line(context: Any, content: str) -> None:
context.fix_input = content
@then("the zero-colon line should be preserved verbatim")
def step_assert_zero_colon_preserved(context: Any) -> None:
assert context.fix_output == context.fix_input
# ---------------------------------------------------------------------------
# Lines 236-237 - _sum_filter ValueError
# ---------------------------------------------------------------------------
@given("a sequence of non-summable items")
def step_non_summable_sequence(context: Any) -> None:
# Passing strings that can't be summed with + starting from 0
context.non_summable = "not-a-sequence"
@when("I call the sum filter on the non-summable sequence")
def step_call_sum_filter(context: Any) -> None:
try:
context.result = context.engine._sum_filter(context.non_summable)
context.error = None
except Exception as exc:
context.result = None
context.error = exc
@then("a ValueError should be raised with a descriptive message")
def step_assert_sum_valueerror(context: Any) -> None:
assert isinstance(context.error, ValueError), (
f"Expected ValueError, got {type(context.error)}: {context.error}"
)
assert "Cannot sum value" in str(context.error)
# ---------------------------------------------------------------------------
# Integration: end-to-end fallback
# ---------------------------------------------------------------------------
@given('a template "{template}" with context val set to "{val}"')
def step_template_with_val(context: Any, template: str, val: str) -> None:
context.yaml_template = template
context.template_ctx = {"val": val, "context": {}}
@when("rendering triggers a YAMLError on first parse but fix produces a dict")
def step_render_e2e_fallback(context: Any) -> None:
side_effects = [
yaml.YAMLError("transient parse failure"),
{"result": "ok"},
]
with mock.patch.object(_mod.yaml, "safe_load", side_effect=side_effects):
context.result = context.engine._render_and_parse(
context.yaml_template, context.template_ctx
)
@then("the final result should be the fixed dict")
def step_assert_e2e_fixed(context: Any) -> None:
assert context.result == {"result": "ok"}