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