forked from HAL9000/cleveragents-core
e801eb1ee8
- 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
316 lines
11 KiB
Python
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"
|