Files
cleveragents-core/features/steps/config_resolution_steps.py
T

344 lines
12 KiB
Python

"""Step definitions for config_resolution.feature."""
from __future__ import annotations
import os
import shutil
import tempfile
from pathlib import Path
from typing import Any
from behave import given, then, use_step_matcher, when
from behave.runner import Context
from cleveragents.application.services.config_service import ConfigService
use_step_matcher("re")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_service(context: Context) -> ConfigService:
"""Return the test-scoped ConfigService instance."""
svc: ConfigService = context._config_service
return svc
def _cleanup(context: Context) -> None:
"""Clean up temp dir and env vars."""
tmpdir = getattr(context, "_config_tmpdir", None)
if tmpdir and os.path.isdir(tmpdir):
shutil.rmtree(tmpdir, ignore_errors=True)
for var in getattr(context, "_env_vars_set", []):
os.environ.pop(var, None)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a clean config service test environment")
def step_clean_config_service_env(context: Context) -> None:
tmpdir = tempfile.mkdtemp()
context._config_tmpdir = tmpdir
tmp_path = Path(tmpdir)
context._config_service = ConfigService(
config_dir=tmp_path,
config_path=tmp_path / "config.toml",
)
context._env_vars_set = []
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers = []
context._cleanup_handlers.append(lambda: _cleanup(context))
# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------
@given(
r'I have written global config key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
)
def step_write_global_config(context: Context, key: str, value: str) -> None:
svc = _get_service(context)
data = svc.read_config()
data[key] = value
svc.write_config(data)
@given(
r'I have written project "(?P<project>[^"]+)" config key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
)
def step_write_project_config(
context: Context, project: str, key: str, value: str
) -> None:
svc = _get_service(context)
data = svc.read_config()
if "project" not in data:
data["project"] = {}
proj_data: dict[str, Any] = data["project"]
if project not in proj_data:
proj_data[project] = {}
proj_data[project][key] = value
svc.write_config(data)
@given(r'I write config data with key "(?P<key>[^"]+)" and value "(?P<value>[^"]+)"')
def step_write_config_data(context: Context, key: str, value: str) -> None:
svc = _get_service(context)
svc.set_value(key, value)
# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------
@when(r'I resolve config key "(?P<key>[^"]+)" with CLI value "(?P<cli_value>[^"]+)"')
def step_resolve_key_cli(context: Context, key: str, cli_value: str) -> None:
svc = _get_service(context)
context.resolved = svc.resolve(key, cli_value=cli_value)
@when(r'I resolve config key "(?P<key>[^"]+)" for project "(?P<project>[^"]+)"')
def step_resolve_key_project(context: Context, key: str, project: str) -> None:
svc = _get_service(context)
context.resolved = svc.resolve(key, project_name=project)
@when(r'I resolve config key "(?P<key>[^"]+)" with verbose mode')
def step_resolve_key_verbose(context: Context, key: str) -> None:
svc = _get_service(context)
context.resolved = svc.resolve(key, verbose=True)
@when(r'I resolve config key "(?P<key>[^"]+)"')
def step_resolve_key(context: Context, key: str) -> None:
svc = _get_service(context)
context.resolved = svc.resolve(key)
@when(r'I look up the env var for key "(?P<key>[^"]+)"')
def step_lookup_env_var(context: Context, key: str) -> None:
svc = _get_service(context)
context.env_var_name = svc.env_var_for_key(key)
@when(r'I attempt to resolve unknown key "(?P<key>[^"]+)"')
def step_attempt_resolve_unknown(context: Context, key: str) -> None:
svc = _get_service(context)
try:
svc.resolve(key)
context.raised_error = None
except ValueError as exc:
context.raised_error = exc
@when(r'I attempt to validate unknown key "(?P<key>[^"]+)"')
def step_attempt_validate_unknown(context: Context, key: str) -> None:
try:
ConfigService.validate_key(key)
context.raised_error = None
except ValueError as exc:
context.raised_error = exc
@when(
r'I attempt to validate type for key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
)
def step_attempt_validate_type(context: Context, key: str, value: str) -> None:
try:
ConfigService.validate_type(key, value)
context.raised_error = None
except (TypeError, ValueError) as exc:
context.raised_error = exc
@when("I read the config file")
def step_read_config_file(context: Context) -> None:
svc = _get_service(context)
context.config_data = svc.read_config()
@when("I write config data to a new directory")
def step_write_to_new_dir(context: Context) -> None:
base = Path(context._config_tmpdir) / "nested" / "subdir"
context._new_config_dir = base
svc = ConfigService(config_dir=base, config_path=base / "config.toml")
svc.write_config({"test_key": "test_value"})
@when("I resolve all keys")
def step_resolve_all(context: Context) -> None:
svc = _get_service(context)
context.all_resolved = svc.resolve_all()
@when(r'I set config value "(?P<key>[^"]+)" to "(?P<value>[^"]+)"')
def step_set_config_value(context: Context, key: str, value: str) -> None:
svc = _get_service(context)
svc.set_value(key, value)
@when(r'I get entry for key "(?P<key>[^"]+)"')
def step_get_entry(context: Context, key: str) -> None:
context.entry = ConfigService.get_entry(key)
# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------
@then(r'the resolved value should be "(?P<value>[^"]+)"')
def step_resolved_value(context: Context, value: str) -> None:
assert str(context.resolved.value) == value, (
f"Expected '{value}', got '{context.resolved.value}'"
)
@then(r'the resolved source should be "(?P<source>[^"]+)"')
def step_resolved_source(context: Context, source: str) -> None:
assert context.resolved.source.value == source, (
f"Expected source '{source}', got '{context.resolved.source.value}'"
)
@then(r'the env var name should be "(?P<expected>[^"]+)"')
def step_env_var_name(context: Context, expected: str) -> None:
assert context.env_var_name == expected, (
f"Expected '{expected}', got '{context.env_var_name}'"
)
@then(r'the config ValueError message should contain "(?P<fragment>[^"]+)"')
def step_config_value_error_raised(context: Context, fragment: str) -> None:
assert context.raised_error is not None, "Expected ValueError but none was raised"
assert isinstance(context.raised_error, ValueError), (
f"Expected ValueError, got {type(context.raised_error).__name__}"
)
assert fragment in str(context.raised_error), (
f"Expected '{fragment}' in message: {context.raised_error}"
)
@then(r'the config TypeError message should contain "(?P<fragment>[^"]+)"')
def step_config_type_error_raised(context: Context, fragment: str) -> None:
assert context.raised_error is not None, "Expected TypeError but none was raised"
assert isinstance(context.raised_error, TypeError), (
f"Expected TypeError, got {type(context.raised_error).__name__}"
)
assert fragment in str(context.raised_error), (
f"Expected '{fragment}' in message: {context.raised_error}"
)
@then(r"the resolved value should be integer (?P<value>\d+)")
def step_resolved_int(context: Context, value: str) -> None:
expected = int(value)
assert context.resolved.value == expected, (
f"Expected {expected}, got {context.resolved.value}"
)
assert isinstance(context.resolved.value, int), (
f"Expected int, got {type(context.resolved.value).__name__}"
)
@then("the resolved value should be boolean true")
def step_resolved_bool_true(context: Context) -> None:
assert context.resolved.value is True, (
f"Expected True, got {context.resolved.value}"
)
@then("the resolved value should be boolean false")
def step_resolved_bool_false(context: Context) -> None:
assert context.resolved.value is False, (
f"Expected False, got {context.resolved.value}"
)
@then(r"the resolved value should be float (?P<value>[\d.]+)")
def step_resolved_float(context: Context, value: str) -> None:
expected = float(value)
assert abs(context.resolved.value - expected) < 0.001, (
f"Expected {expected}, got {context.resolved.value}"
)
@then(r"the resolution chain should have (?P<count>\d+) entries")
def step_chain_length(context: Context, count: str) -> None:
expected = int(count)
assert len(context.resolved.chain) == expected, (
f"Expected {expected} entries, got {len(context.resolved.chain)}"
)
@then(r'the chain should include source "(?P<source>[^"]+)"')
def step_chain_includes_source(context: Context, source: str) -> None:
sources = [entry["source"] for entry in context.resolved.chain]
assert source in sources, f"Expected source '{source}' in chain: {sources}"
@then(r'the registry should contain key "(?P<key>[^"]+)"')
def step_registry_contains(context: Context, key: str) -> None:
registry = ConfigService.registry()
assert key in registry, f"Key '{key}' not found in registry"
@then(
r'the config data should contain key "(?P<key>[^"]+)" with value "(?P<value>[^"]+)"'
)
def step_config_data_contains(context: Context, key: str, value: str) -> None:
data = context.config_data
assert key in data, f"Key '{key}' not in config data: {data}"
assert str(data[key]) == value, (
f"Expected '{value}' for key '{key}', got '{data[key]}'"
)
@then("the config directory should exist")
def step_config_dir_exists(context: Context) -> None:
assert context._new_config_dir.exists(), (
f"Config dir {context._new_config_dir} does not exist"
)
@then("the result should contain all registered keys")
def step_result_has_all_keys(context: Context) -> None:
registered = ConfigService.registered_keys()
for key in registered:
assert key in context.all_resolved, (
f"Key '{key}' missing from resolve_all result"
)
@then(r'the entry should have section "(?P<section>[^"]+)"')
def step_entry_section(context: Context, section: str) -> None:
assert context.entry is not None, "Entry should not be None"
assert context.entry.section == section, (
f"Expected section '{section}', got '{context.entry.section}'"
)
@then(r'the entry should have python type "(?P<type_name>[^"]+)"')
def step_entry_type(context: Context, type_name: str) -> None:
assert context.entry is not None, "Entry should not be None"
assert context.entry.python_type.__name__ == type_name, (
f"Expected type '{type_name}', got '{context.entry.python_type.__name__}'"
)
@then("the entry should be None")
def step_entry_is_none(context: Context) -> None:
assert context.entry is None, f"Expected None, got {context.entry}"
# Restore default step matcher for other step files
use_step_matcher("parse")