forked from HAL9000/cleveragents-core
a808c395f9
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
231 lines
8.6 KiB
Python
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"}
|