Files
cleveragents-core/features/steps/actor_config_steps.py
T
hurui200320 d512123d1c
CI / lint (pull_request) Successful in 48s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 54s
CI / quality (pull_request) Successful in 1m52s
CI / typecheck (pull_request) Successful in 1m57s
CI / security (pull_request) Successful in 1m58s
CI / helm (pull_request) Successful in 27s
CI / push-validation (pull_request) Successful in 21s
CI / integration_tests (pull_request) Successful in 6m43s
CI / unit_tests (pull_request) Successful in 9m4s
CI / docker (pull_request) Successful in 1m42s
CI / coverage (pull_request) Successful in 11m45s
CI / e2e_tests (pull_request) Successful in 3m22s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Failing after 44s
CI / build (push) Successful in 49s
CI / lint (push) Successful in 1m8s
CI / helm (push) Successful in 38s
CI / quality (push) Successful in 1m25s
CI / security (push) Successful in 1m26s
CI / typecheck (push) Successful in 1m30s
CI / push-validation (push) Successful in 23s
CI / e2e_tests (push) Successful in 3m52s
CI / integration_tests (push) Successful in 4m8s
CI / coverage (push) Successful in 12m30s
CI / unit_tests (push) Successful in 6m32s
CI / docker (push) Successful in 1m39s
CI / status-check (push) Successful in 3s
fix(actor): support v3 Actor YAML schema in CLI registration and execution
The actor CLI was ignoring the v3 ActorConfigSchema format, preventing
spec-compliant actors with type/route/skills/lsp fields from being
registered or executed.  Three components were fixed:

ActorConfiguration.from_blob() now detects v3 format (top-level "type"
key with value llm/graph/tool) and extracts provider from the model
string, falling through to v2 extraction when v3 does not match.

ActorRegistry.add() now routes v3 YAML through full ActorConfigSchema
validation, persists description/skills/lsp in the config blob, and
compiles graph actors with compile_actor() storing metadata.  Legacy v2
YAML continues through the original path unchanged.

ReactiveConfigParser._build() now synthesises reactive agents and graph
routes from v3 actor data so that agents actor run can execute v3 actors
through the existing ReactiveCleverAgentsApp pipeline.

ISSUES CLOSED: #6283
2026-04-27 05:00:54 +00:00

285 lines
10 KiB
Python

"""Step definitions for actor configuration coverage."""
from __future__ import annotations
import ast
import json
import os
import shutil
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any
from behave import given, then, when
from cleveragents.actor.config import ActorConfiguration
def _add_cleanup(context, handler: Callable[[], None]) -> None:
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(handler)
def _ensure_workspace(context) -> Path:
if getattr(context, "config_workspace", None) is None:
workspace = Path(tempfile.mkdtemp(prefix="actor-config-"))
context.config_workspace = workspace
def cleanup_workspace() -> None:
shutil.rmtree(workspace, ignore_errors=True)
_add_cleanup(context, cleanup_workspace)
return context.config_workspace
def _resolve_path(obj: Any, path: str) -> Any:
current: Any = obj
for segment in path.split("."):
if isinstance(current, list):
current = current[int(segment)]
continue
if isinstance(current, dict):
assert segment in current, f"{segment} missing from {current}"
current = current[segment]
continue
raise AssertionError(f"Cannot traverse through {type(current).__name__}")
return current
@given("an isolated actor config workspace")
def step_isolated_workspace(context) -> None:
_ensure_workspace(context)
@given('an actor config file "{filename}" with content:')
def step_actor_config_file_with_content(context, filename: str) -> None:
workspace = _ensure_workspace(context)
path = workspace / filename
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(context.text or "")
context.last_config_path = path
@given("PyYAML parsing is unavailable for actor config")
def step_disable_yaml(context) -> None:
import cleveragents.actor.yaml_loader as yaml_loader
context._original_yaml_module = yaml_loader.yaml
yaml_loader.yaml = None
def restore_yaml() -> None:
yaml_loader.yaml = context._original_yaml_module
_add_cleanup(context, restore_yaml)
@given('the actor config environment variable "{name}" is unset')
def step_unset_env_var(context, name: str) -> None:
original = os.environ.get(name)
def restore() -> None:
if original is None:
os.environ.pop(name, None)
else:
os.environ[name] = original
_add_cleanup(context, restore)
os.environ.pop(name, None)
@given('the actor config environment variable "{name}" is set to "{value}"')
def step_set_env_var(context, name: str, value: str) -> None:
original = os.environ.get(name)
def restore() -> None:
if original is None:
os.environ.pop(name, None)
else:
os.environ[name] = original
_add_cleanup(context, restore)
os.environ[name] = value
@when('I load the actor config blob from "{filename}"')
def step_load_actor_config_blob(context, filename: str) -> None:
workspace = _ensure_workspace(context)
path = workspace / filename
try:
context.load_result = ActorConfiguration.load_blob_from_file(path)
context.last_error = None
except Exception as exc: # pragma: no cover - exercised in tests
context.load_result = None
context.last_error = exc
@when('I parse the actor configuration from file "{filename}" with overrides:')
def step_parse_actor_config_from_file(context, filename: str) -> None:
workspace = _ensure_workspace(context)
overrides: dict[str, Any] = json.loads(context.text or "{}")
try:
context.actor_config_result = ActorConfiguration.from_file(
path=workspace / filename,
name=overrides.get("name"),
provider=overrides.get("provider"),
model=overrides.get("model"),
graph_descriptor=overrides.get("graph_descriptor"),
unsafe=bool(overrides.get("unsafe", False)),
)
context.last_error = None
except Exception as exc: # pragma: no cover - exercised in tests
context.actor_config_result = None
context.last_error = exc
@when("I build an actor configuration from blob {blob_literal}")
def step_build_actor_config_from_blob(context, blob_literal: str) -> None:
blob = ast.literal_eval(blob_literal)
try:
context.actor_config_result = ActorConfiguration.from_blob(blob=blob)
context.last_error = None
except Exception as exc: # pragma: no cover - exercised in tests
context.actor_config_result = None
context.last_error = exc
@when(
"I build an actor configuration from structured blob with defaults and overrides:"
)
def step_build_actor_config_with_defaults(context) -> None:
payload = json.loads(context.text or "{}")
blob = payload.get("blob")
default_options = payload.get("default_options")
option_overrides = payload.get("option_overrides")
try:
context.actor_config_result = ActorConfiguration.from_blob(
blob=blob,
default_options=default_options,
option_overrides=option_overrides,
)
context.last_error = None
except Exception as exc: # pragma: no cover - exercised in tests
context.actor_config_result = None
context.last_error = exc
@then('a ValueError should be raised containing "{message}"')
def step_value_error_with_message(context, message: str) -> None:
assert isinstance(context.last_error, ValueError), type(context.last_error)
assert message in str(context.last_error)
@then("the loaded actor config blob should equal {expected}")
def step_loaded_blob_equals(context, expected: str) -> None:
assert context.last_error is None, context.last_error
assert context.load_result == ast.literal_eval(expected)
@then('the loaded actor config value at "{path_expr}" should equal {expected}')
def step_loaded_blob_value_at_path(context, path_expr: str, expected: str) -> None:
assert context.last_error is None, context.last_error
assert context.load_result is not None
actual = _resolve_path(context.load_result, path_expr)
assert actual == ast.literal_eval(expected)
@then('the actor configuration should have provider "{provider}" and model "{model}"')
def step_actor_config_provider_model(context, provider: str, model: str) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
assert context.actor_config_result.provider == provider
assert context.actor_config_result.model == model
@then('the actor configuration name should be "{name}"')
def step_actor_config_name(context, name: str) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
assert context.actor_config_result.name == name
@then("the actor configuration graph descriptor should equal {expected}")
def step_actor_config_graph_descriptor(context, expected: str) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
assert context.actor_config_result.graph_descriptor == ast.literal_eval(expected)
@then('the actor configuration graph descriptor should include key "{key}"')
def step_actor_config_graph_descriptor_has_key(context, key: str) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
graph_descriptor = context.actor_config_result.graph_descriptor
assert graph_descriptor is not None, "graph_descriptor missing"
assert key in graph_descriptor, f"{key} missing from graph_descriptor"
@then("the actor configuration options should equal {expected}")
def step_actor_config_options(context, expected: str) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
assert context.actor_config_result.options == ast.literal_eval(expected)
@then('the loaded actor config value at "{path_expr}" should contain "{substring}"')
def step_loaded_blob_value_contains(context, path_expr: str, substring: str) -> None:
assert context.last_error is None, context.last_error
assert context.load_result is not None
actual = _resolve_path(context.load_result, path_expr)
assert isinstance(actual, str), f"Expected str, got {type(actual).__name__}"
assert substring in actual, f"'{substring}' not found in '{actual}'"
@then('the loaded actor config nested value at "{path_expr}" should equal {expected}')
def step_loaded_blob_nested_value(context, path_expr: str, expected: str) -> None:
assert context.last_error is None, context.last_error
assert context.load_result is not None
actual = _resolve_path(context.load_result, path_expr)
assert actual == ast.literal_eval(expected), f"Expected {expected}, got {actual!r}"
@when("I build actor config from None blob with provider and model overrides")
def step_build_actor_config_none_blob(context) -> None:
try:
context.actor_config_result = ActorConfiguration.from_blob(
blob=None,
provider="override-provider",
model="override-model",
)
context.last_error = None
except Exception as exc: # pragma: no cover - exercised in tests
context.actor_config_result = None
context.last_error = exc
@when("I build actor config from blob using graph key alias")
def step_build_actor_config_graph_alias(context) -> None:
try:
context.actor_config_result = ActorConfiguration.from_blob(
blob={
"provider": "p",
"model": "m",
"graph": {"step": "one"},
},
)
context.last_error = None
except Exception as exc: # pragma: no cover - exercised in tests
context.actor_config_result = None
context.last_error = exc
@then("the actor configuration graph descriptor should be None")
def step_actor_config_graph_descriptor_none(context) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
assert context.actor_config_result.graph_descriptor is None
@then("the actor configuration unsafe flag should be true")
def step_actor_config_unsafe_true(context) -> None:
assert context.last_error is None, context.last_error
assert context.actor_config_result is not None
assert context.actor_config_result.unsafe is True