Files
temp/features/steps/yaml_engine_direct_coverage_steps.py
brent.edwards e801eb1ee8 feat(ci): add nox-based PR validation workflow
- Rewrite .forgejo/workflows/ci.yml to route all jobs through nox sessions
- Fix coverage_report nox session: serial behave mode replaces broken parallel
  mode (22% -> 97% accuracy), raise fail-under from 85% to 97%
- Pass posargs through format nox session for CI --check support
- Add 11 CI workflow validation scenarios (Behave) + Robot smoke test + ASV bench
- Add 108 new Behave scenarios covering 6 largest coverage gaps to reach 97%:
  yaml_template_engine, actor/config, actor/registry, message_router,
  context_analysis, context_service
- Update docs/development/ci-cd.md with nox-based CI docs and 97% threshold
- Restore implementation_plan.md verbose style, check off completed CI tasks

Verified: 1673 scenarios pass, 97% coverage, lint clean, typecheck clean
2026-02-12 22:01:51 +00:00

316 lines
11 KiB
Python

"""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"