Files
temp/features/steps/actor_config_new_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

388 lines
13 KiB
Python

"""Step definitions for actor_config_new_coverage.feature."""
from __future__ import annotations
import json
import os
import tempfile
from pathlib import Path
from typing import Any
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from cleveragents.actor.config import ActorConfiguration
@then('an actor config ValueError should mention "{text}"')
def step_assert_actor_config_valueerror(context: Context, text: str) -> None:
assert context.error is not None, "Expected ValueError but none was raised"
assert text.lower() in str(context.error).lower(), (
f"Expected '{text}' in error: {context.error}"
)
@given("a temporary JSON config file with provider and model")
def step_temp_json_config(context: Context) -> None:
data = {"provider": "openai", "model": "gpt-4", "options": {"temperature": 0.7}}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
) as tmp:
json.dump(data, tmp)
tmp.flush()
context.temp_config_path = Path(tmp.name)
@when("I load the blob from the JSON file")
def step_load_blob_json(context: Context) -> None:
context.blob = ActorConfiguration.load_blob_from_file(context.temp_config_path)
@then("the blob should contain provider and model keys")
def step_assert_blob_keys(context: Context) -> None:
assert "provider" in context.blob
assert "model" in context.blob
@when("I load the blob from a nonexistent path")
def step_load_blob_missing(context: Context) -> None:
context.error = None
try:
ActorConfiguration.load_blob_from_file(
Path("/tmp/nonexistent_config_12345.json")
)
except ValueError as exc:
context.error = exc
@given("a temporary JSON config file with a list")
def step_temp_json_list(context: Context) -> None:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
) as tmp:
json.dump([1, 2, 3], tmp)
tmp.flush()
context.temp_list_path = Path(tmp.name)
@when("I load the blob from the list JSON file")
def step_load_blob_list(context: Context) -> None:
context.error = None
try:
ActorConfiguration.load_blob_from_file(context.temp_list_path)
except ValueError as exc:
context.error = exc
@given("a temporary JSON config file with null content")
def step_temp_json_null(context: Context) -> None:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
) as tmp:
tmp.write("null")
tmp.flush()
context.temp_null_path = Path(tmp.name)
@when("I load the blob from the null JSON file")
def step_load_blob_null(context: Context) -> None:
context.blob = ActorConfiguration.load_blob_from_file(context.temp_null_path)
@then("the blob should be an empty dict")
def step_assert_empty_dict(context: Context) -> None:
assert context.blob == {}
@given("a temporary YAML-only config file with provider and model")
def step_temp_yaml_config(context: Context) -> None:
content = "provider: anthropic\nmodel: claude-3\noptions:\n temperature: 0.5\n"
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False, encoding="utf-8"
) as tmp:
tmp.write(content)
tmp.flush()
context.temp_yaml_path = Path(tmp.name)
@when("I load the blob from the YAML file")
def step_load_blob_yaml(context: Context) -> None:
context.blob = ActorConfiguration.load_blob_from_file(context.temp_yaml_path)
@when("I load v2 YAML content without templates")
def step_load_v2_plain(context: Context) -> None:
text = "provider: openai\nmodel: gpt-4\n"
context.result = ActorConfiguration._load_v2_yaml_content(text)
@then("the v2 result should be a dict with expected keys")
def step_assert_dict_expected_keys(context: Context) -> None:
assert isinstance(context.result, dict)
assert len(context.result) > 0
@when("I load v2 YAML content that is empty")
def step_load_v2_empty(context: Context) -> None:
context.result = ActorConfiguration._load_v2_yaml_content("")
@then("the v2 result should be an empty dict")
def step_assert_empty_result(context: Context) -> None:
assert context.result == {}
@when("I load v2 YAML content that is a list")
def step_load_v2_list(context: Context) -> None:
context.error = None
try:
ActorConfiguration._load_v2_yaml_content("- item1\n- item2\n")
except ValueError as exc:
context.error = exc
@when("I load v2 YAML content with Jinja2 templates")
def step_load_v2_templated(context: Context) -> None:
text = 'provider: openai\nmodel: gpt-4\nsystem_prompt: "Hello {{ context.name }}"\n'
context.result = ActorConfiguration._load_v2_yaml_content(text)
@then("the actor config result should be a dict")
def step_assert_is_dict(context: Context) -> None:
assert isinstance(context.result, dict)
@when("I restore template syntax in a config with protected markers")
def step_restore_markers(context: Context) -> None:
config: dict[str, Any] = {
"system_prompt": "Hello <<<TEMPLATE_START>>>name<<<TEMPLATE_END>>>",
"other": "<<<BLOCK_START>>> for x <<<BLOCK_END>>>",
}
context.result = ActorConfiguration._restore_template_syntax(config)
@then("the system_prompt should have Jinja2 syntax restored")
def step_assert_restored_prompt(context: Context) -> None:
assert "{{" in context.result["system_prompt"]
assert "}}" in context.result["system_prompt"]
@when("I restore template syntax in a list config")
def step_restore_list(context: Context) -> None:
config = [
{"system_prompt": "<<<TEMPLATE_START>>>x<<<TEMPLATE_END>>>"},
"plain",
]
context.result = ActorConfiguration._restore_template_syntax(config)
@then("nested list items should have markers restored")
def step_assert_list_restored(context: Context) -> None:
assert isinstance(context.result, list)
assert "{{" in context.result[0]["system_prompt"]
@given('environment variable "{name}" is set to "{value}"')
def step_set_env_var(context: Context, name: str, value: str) -> None:
os.environ[name] = value
if not hasattr(context, "env_cleanup"):
context.env_cleanup = []
context.env_cleanup.append(name)
@when('I interpolate env vars in config with "${{{var_expr}}}"')
def step_interpolate_env(context: Context, var_expr: str) -> None:
config = f"${{{var_expr}}}"
context.result = ActorConfiguration._interpolate_env_vars(config)
@then('the interpolated value should be "{expected}"')
def step_assert_interpolated(context: Context, expected: str) -> None:
assert str(context.result) == expected, (
f"Expected '{expected}', got '{context.result}'"
)
@when('I interpolate env vars with "${{{var_expr}}}" and no default')
def step_interpolate_no_default(context: Context, var_expr: str) -> None:
config = f"${{{var_expr}}}"
context.error = None
try:
ActorConfiguration._interpolate_env_vars(config)
except ValueError as exc:
context.error = exc
@when('I interpolate env vars for string "{value}"')
def step_interpolate_literal(context: Context, value: str) -> None:
context.result = ActorConfiguration._interpolate_env_vars(value)
@then("the interpolated result should be boolean True")
def step_assert_bool_true(context: Context) -> None:
assert context.result is True
@then("the interpolated result should be integer {n:d}")
def step_assert_int(context: Context, n: int) -> None:
assert context.result == n
assert isinstance(context.result, int)
@then("the interpolated result should be float {f}")
def step_assert_float(context: Context, f: str) -> None:
assert abs(context.result - float(f)) < 0.001
assert isinstance(context.result, float)
@then('the interpolated result should be string "{expected}"')
def step_assert_string(context: Context, expected: str) -> None:
assert str(context.result) == expected
@when("I interpolate env vars in a nested structure")
def step_interpolate_nested(context: Context) -> None:
config: dict[str, Any] = {
"provider": "openai",
"items": ["first", "second"],
"nested": {"key": "value"},
}
context.result = ActorConfiguration._interpolate_env_vars(config)
@then("all nested string values should be interpolated")
def step_assert_nested_interpolated(context: Context) -> None:
assert isinstance(context.result, dict)
assert isinstance(context.result["items"], list)
assert isinstance(context.result["nested"], dict)
@when("I create an ActorConfiguration from a blob with provider and model")
def step_from_blob(context: Context) -> None:
blob: dict[str, Any] = {"provider": "openai", "model": "gpt-4"}
context.actor_cfg = ActorConfiguration.from_blob(blob=blob)
@then("the config provider should be set")
def step_assert_config_provider(context: Context) -> None:
assert context.actor_cfg.provider
@then("the config model should be set")
def step_assert_config_model(context: Context) -> None:
assert context.actor_cfg.model
@when("I create an ActorConfiguration from a blob without provider")
def step_from_blob_no_provider(context: Context) -> None:
context.error = None
try:
ActorConfiguration.from_blob(blob={"model": "gpt-4"})
except ValueError as exc:
context.error = exc
@when("I create an ActorConfiguration from a blob without model")
def step_from_blob_no_model(context: Context) -> None:
context.error = None
try:
ActorConfiguration.from_blob(blob={"provider": "openai"})
except ValueError as exc:
context.error = exc
@when("I create an ActorConfiguration from a blob with option overrides")
def step_from_blob_options(context: Context) -> None:
blob: dict[str, Any] = {"provider": "openai", "model": "gpt-4"}
context.actor_cfg = ActorConfiguration.from_blob(
blob=blob, option_overrides={"temperature": 0.9}
)
@then("the config options should include the overrides")
def step_assert_options(context: Context) -> None:
assert context.actor_cfg.options["temperature"] == 0.9
@when("I extract v2 actor from a v2-style config with agents block")
def step_extract_v2_actor(context: Context) -> None:
data: dict[str, Any] = {
"agents": {
"main_agent": {
"config": {
"provider": "anthropic",
"model": "claude-3",
"unsafe": True,
"options": {"max_tokens": 1000},
}
}
},
"routes": [{"from": "main_agent", "to": "end"}],
}
context.extracted = ActorConfiguration._extract_v2_actor(data)
@then("the extracted provider and model should be set")
def step_assert_extracted(context: Context) -> None:
provider, model, _graph, unsafe = context.extracted
assert provider == "anthropic"
assert model == "claude-3"
assert unsafe is True
@then("the graph descriptor should contain agent info")
def step_assert_graph_descriptor(context: Context) -> None:
_, _, graph, _ = context.extracted
assert graph is not None
assert "agent" in graph
assert "routes" in graph
@when("I extract v2 actor from a config without agents block")
def step_extract_v2_no_agents(context: Context) -> None:
context.extracted = ActorConfiguration._extract_v2_actor({"provider": "openai"})
@then("all extracted values should be None")
def step_assert_all_none(context: Context) -> None:
provider, model, graph, unsafe = context.extracted
assert provider is None
assert model is None
assert graph is None
assert unsafe is False
@when("I extract v2 options from a v2-style config")
def step_extract_v2_options(context: Context) -> None:
data: dict[str, Any] = {
"agents": {
"main_agent": {
"config": {
"provider": "openai",
"model": "gpt-4",
"options": {"temperature": 0.7, "max_tokens": 500},
}
}
}
}
context.options = ActorConfiguration._extract_v2_options(data)
@then("the extracted options should contain expected keys")
def step_assert_extracted_options(context: Context) -> None:
assert context.options is not None
assert "temperature" in context.options
@when("I create an ActorConfiguration from the file")
def step_from_file(context: Context) -> None:
context.actor_cfg = ActorConfiguration.from_file(
path=context.temp_config_path,
)
@when('I interpolate env vars with "${{{var_expr}}}"')
def step_interpolate_with_default(context: Context, var_expr: str) -> None:
config = f"${{{var_expr}}}"
context.result = ActorConfiguration._interpolate_env_vars(config)