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
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
285 lines
10 KiB
Python
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
|