Files
cleveragents-core/features/steps/config_service_coverage_steps.py
T
freemo 7b3fcaf466
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 19s
CI / helm (pull_request) Successful in 20s
CI / lint (pull_request) Successful in 3m19s
CI / quality (pull_request) Successful in 3m49s
CI / typecheck (pull_request) Successful in 3m57s
CI / integration_tests (pull_request) Successful in 3m57s
CI / security (pull_request) Successful in 4m6s
CI / unit_tests (pull_request) Successful in 7m19s
CI / docker (pull_request) Successful in 1m18s
CI / coverage (pull_request) Successful in 11m53s
CI / e2e_tests (pull_request) Successful in 21m25s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 18s
CI / helm (push) Successful in 22s
CI / lint (push) Successful in 3m16s
CI / quality (push) Successful in 3m41s
CI / integration_tests (push) Successful in 3m48s
CI / typecheck (push) Successful in 3m54s
CI / unit_tests (push) Successful in 3m54s
CI / security (push) Successful in 4m4s
CI / docker (push) Successful in 1m19s
CI / benchmark-regression (push) Has been skipped
CI / e2e_tests (push) Successful in 14m58s
CI / coverage (push) Successful in 11m55s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Successful in 28m23s
CI / benchmark-regression (pull_request) Successful in 54m47s
feat(config): implement three-scope config resolution with local config file support
Implement three-scope configuration resolution (local > project > global)
with deep merge. Add ConfigScope enum, project-root discovery, and
config.local.toml file support.

Changes:
- Add LOCAL to ConfigLevel enum and new ConfigScope enum (GLOBAL, PROJECT, LOCAL)
- Implement discover_project_root() walking up from CWD for cleveragents.toml/.cleveragents
- Implement config.local.toml file loading in ConfigService
- Implement three-scope deep merge via _deep_merge() helper
- Add read_project_config(), read_local_config(), read_merged_config() methods
- Add write_scoped_config() for writing to any scope file
- Update set_value() to accept optional scope parameter
- Update resolve() with six-level precedence chain (CLI > env > local > project > global > default)
- Update CLI config set with --scope flag (global/project/local)
- Add config.local.toml to .gitignore and DEFAULT_IGNORE_PATTERNS
- Add Behave BDD scenarios for three-scope resolution, deep merge, and project-root discovery

ISSUES CLOSED: #937
2026-03-30 21:48:01 +00:00

832 lines
29 KiB
Python

"""Behave steps for complete coverage of config_service.py."""
from __future__ import annotations
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import patch
import tomlkit
from behave import given, then, when
from cleveragents.application.services.config_service import (
_REGISTRY,
ConfigEntry,
ConfigLevel,
ConfigService,
ResolvedValue,
_env_name,
_register,
)
# ---------------------------------------------------------------------------
# ConfigLevel enum
# ---------------------------------------------------------------------------
@then("ConfigLevel should have values CLI_FLAG ENV_VAR LOCAL PROJECT GLOBAL DEFAULT")
def step_config_level_enum(context: Any) -> None:
assert ConfigLevel.CLI_FLAG.value == "cli_flag"
assert ConfigLevel.ENV_VAR.value == "env_var"
assert ConfigLevel.LOCAL.value == "local"
assert ConfigLevel.PROJECT.value == "project"
assert ConfigLevel.GLOBAL.value == "global"
assert ConfigLevel.DEFAULT.value == "default"
assert len(ConfigLevel) == 6
# ---------------------------------------------------------------------------
# ConfigEntry dataclass
# ---------------------------------------------------------------------------
@given('a ConfigEntry with key "test.key" and type str')
def step_create_config_entry(context: Any) -> None:
context.entry = ConfigEntry(
key="test.key",
python_type=str,
default="default_val",
env_var="CLEVERAGENTS_TEST_KEY",
project_scopable=True,
description="A test key.",
section="test",
)
@then("the ConfigEntry fields should match the provided values")
def step_verify_config_entry(context: Any) -> None:
e = context.entry
assert e.key == "test.key"
assert e.python_type is str
assert e.default == "default_val"
assert e.env_var == "CLEVERAGENTS_TEST_KEY"
assert e.project_scopable is True
assert e.description == "A test key."
assert e.section == "test"
# ---------------------------------------------------------------------------
# ResolvedValue dataclass
# ---------------------------------------------------------------------------
@given('a ResolvedValue with key "test.key" and source DEFAULT')
def step_create_resolved_value(context: Any) -> None:
context.resolved = ResolvedValue(
key="test.key",
value="default_val",
source=ConfigLevel.DEFAULT,
)
@then("the ResolvedValue fields should match including an empty chain by default")
def step_verify_resolved_value(context: Any) -> None:
rv = context.resolved
assert rv.key == "test.key"
assert rv.value == "default_val"
assert rv.source == ConfigLevel.DEFAULT
assert rv.chain == []
# ---------------------------------------------------------------------------
# _env_name helper
# ---------------------------------------------------------------------------
@when('I call _env_name with section "core" and key "log_level"')
def step_call_env_name(context: Any) -> None:
context.env_name_result = _env_name("core", "log_level")
@then('the env name should be "CLEVERAGENTS_CORE_LOG_LEVEL"')
def step_verify_env_name(context: Any) -> None:
assert context.env_name_result == "CLEVERAGENTS_CORE_LOG_LEVEL"
# ---------------------------------------------------------------------------
# _register
# ---------------------------------------------------------------------------
@when('I register a custom key "custom.test_key" with type int and default 42')
def step_register_custom_key(context: Any) -> None:
_register("custom", "test_key", int, 42, description="Custom test key.")
@then('the registry should contain "custom.test_key" with default 42')
def step_verify_custom_key(context: Any) -> None:
entry = _REGISTRY.get("custom.test_key")
assert entry is not None
assert entry.default == 42
assert entry.python_type is int
assert entry.env_var == "CLEVERAGENTS_CUSTOM_TEST_KEY"
@when('I register a key "custom.explicit_env" with explicit env_var "MY_CUSTOM_VAR"')
def step_register_explicit_env(context: Any) -> None:
_register(
"custom",
"explicit_env",
str,
"",
env_var="MY_CUSTOM_VAR",
description="Explicit env var.",
)
@then('the entry "custom.explicit_env" should have env_var "MY_CUSTOM_VAR"')
def step_verify_explicit_env(context: Any) -> None:
entry = _REGISTRY.get("custom.explicit_env")
assert entry is not None
assert entry.env_var == "MY_CUSTOM_VAR"
# ---------------------------------------------------------------------------
# _build_catalog
# ---------------------------------------------------------------------------
@then(
"the registry should contain keys from sections core plan provider sandbox context index"
)
def step_verify_catalog_sections(context: Any) -> None:
sections_found: set[str] = set()
for entry in _REGISTRY.values():
sections_found.add(entry.section)
for expected in ("core", "plan", "provider", "sandbox", "context", "index"):
assert expected in sections_found, f"Section '{expected}' not found in registry"
@then("the registry should contain at least 20 keys")
def step_verify_catalog_count(context: Any) -> None:
# _build_catalog registers ~105 keys from the source
assert len(_REGISTRY) >= 20, f"Expected >=20 keys, got {len(_REGISTRY)}"
# ---------------------------------------------------------------------------
# ConfigService instantiation
# ---------------------------------------------------------------------------
@given("a temporary directory for config service")
def step_temp_dir(context: Any) -> None:
context.temp_dir = Path(tempfile.mkdtemp())
context.config_dir = context.temp_dir / "config"
context.config_path = context.config_dir / "config.toml"
context.svc = None
context.result = None
context.error = None
@when("I create a ConfigService with custom paths")
def step_create_service_custom(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_path,
)
@then("the service should use the custom config directory and path")
def step_verify_custom_paths(context: Any) -> None:
assert context.svc._config_dir == context.config_dir
assert context.svc._config_path == context.config_path
@when("I create a ConfigService with default paths")
def step_create_service_default(context: Any) -> None:
context.svc = ConfigService()
@then('the service config path should end with ".cleveragents/config.toml"')
def step_verify_default_path(context: Any) -> None:
path_str = str(context.svc._config_path)
assert path_str.endswith(".cleveragents/config.toml"), f"Got: {path_str}"
# ---------------------------------------------------------------------------
# registry(), get_entry(), registered_keys()
# ---------------------------------------------------------------------------
@when("I call ConfigService.registry")
def step_call_registry(context: Any) -> None:
context.result = ConfigService.registry()
@then("it should return a dict with the same keys as the internal registry")
def step_verify_registry_copy(context: Any) -> None:
assert isinstance(context.result, dict)
assert set(context.result.keys()) == set(_REGISTRY.keys())
# Verify it's a copy, not the same object
assert context.result is not _REGISTRY
@when('I call ConfigService.get_entry with "core.log.level"')
def step_call_get_entry_known(context: Any) -> None:
context.result = ConfigService.get_entry("core.log.level")
@then('it should return a ConfigEntry with key "core.log.level"')
def step_verify_get_entry_known(context: Any) -> None:
assert isinstance(context.result, ConfigEntry)
assert context.result.key == "core.log.level"
@when('I call ConfigService.get_entry with "nonexistent.key"')
def step_call_get_entry_unknown(context: Any) -> None:
context.result = ConfigService.get_entry("nonexistent.key")
@then("it should return None")
def step_verify_get_entry_none(context: Any) -> None:
assert context.result is None
@when("I call ConfigService.registered_keys")
def step_call_registered_keys(context: Any) -> None:
context.result = ConfigService.registered_keys()
@then('it should return a sorted list containing "core.log.level"')
def step_verify_registered_keys(context: Any) -> None:
assert isinstance(context.result, list)
assert context.result == sorted(context.result)
assert "core.log.level" in context.result
# ---------------------------------------------------------------------------
# read_config
# ---------------------------------------------------------------------------
@when("I create a ConfigService pointing to a missing config file")
def step_service_missing_file(context: Any) -> None:
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_dir / "nonexistent.toml",
)
@then("read_config should return an empty dict")
def step_verify_read_empty(context: Any) -> None:
data = context.svc.read_config()
assert data == {}
@given('a TOML config file with key "core.log.level" set to "DEBUG"')
def step_write_toml_log_level(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
doc = tomlkit.document()
doc["core.log.level"] = "DEBUG"
with open(context.config_path, "w") as fh:
tomlkit.dump(doc, fh)
@when("I create a ConfigService pointing to that TOML file")
def step_service_existing_file(context: Any) -> None:
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_path,
)
@then('read_config should return a dict with "core.log.level" equal to "DEBUG"')
def step_verify_read_debug(context: Any) -> None:
data = context.svc.read_config()
assert data.get("core.log.level") == "DEBUG"
# ---------------------------------------------------------------------------
# write_config
# ---------------------------------------------------------------------------
@when("I create a ConfigService with a nested non-existent config directory")
def step_service_nested_dir(context: Any) -> None:
nested = context.temp_dir / "a" / "b" / "c"
context.config_dir = nested
context.config_path = nested / "config.toml"
context.svc = ConfigService(
config_dir=nested,
config_path=context.config_path,
)
@when('I call write_config with key "greeting" value "hello"')
def step_call_write_config(context: Any) -> None:
context.svc.write_config({"greeting": "hello"})
@then('the TOML file should exist and contain key "greeting" with value "hello"')
def step_verify_written_toml(context: Any) -> None:
assert context.config_path.exists()
data = context.svc.read_config()
assert data["greeting"] == "hello"
@given('a TOML config file with key "existing" set to "value1"')
def step_write_toml_existing(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
doc = tomlkit.document()
doc["existing"] = "value1"
with open(context.config_path, "w") as fh:
tomlkit.dump(doc, fh)
@when('I call write_config with key "new_key" value "value2"')
def step_call_write_config_merge(context: Any) -> None:
context.svc.write_config({"new_key": "value2"})
@then('read_config should return a dict containing both "existing" and "new_key"')
def step_verify_merged_toml(context: Any) -> None:
data = context.svc.read_config()
assert data["existing"] == "value1"
assert data["new_key"] == "value2"
# ---------------------------------------------------------------------------
# set_value
# ---------------------------------------------------------------------------
@when('I call set_value with key "mykey" and value "myval"')
def step_call_set_value(context: Any) -> None:
context.svc.set_value("mykey", "myval")
@then('read_config should return a dict with "mykey" equal to "myval"')
def step_verify_set_value(context: Any) -> None:
data = context.svc.read_config()
assert data["mykey"] == "myval"
# ---------------------------------------------------------------------------
# validate_key
# ---------------------------------------------------------------------------
@when('I call validate_key with "core.log.level"')
def step_call_validate_key_known(context: Any) -> None:
try:
context.result = ConfigService.validate_key("core.log.level")
context.error = None
except Exception as exc:
context.error = exc
@then('it should return the ConfigEntry for "core.log.level" without error')
def step_verify_validate_key_ok(context: Any) -> None:
assert context.error is None
assert isinstance(context.result, ConfigEntry)
assert context.result.key == "core.log.level"
@when('I call validate_key with "totally.unknown"')
def step_call_validate_key_unknown(context: Any) -> None:
try:
context.result = ConfigService.validate_key("totally.unknown")
context.error = None
except ValueError as exc:
context.error = exc
@then('a ValueError should be raised mentioning "{text}"')
def step_then_generic_valueerror_raised_mentioning(context: Any, text: str) -> None:
"""Shared @then step: check for a ValueError stored in common context attrs."""
err = None
for attr in ("error", "lsp_error", "caught_error", "ontology_error"):
err = getattr(context, attr, None)
if err is not None:
break
assert err is not None, "Expected a ValueError but none was raised"
assert isinstance(err, ValueError), f"Expected ValueError, got {type(err).__name__}"
assert text in str(err), f"Expected '{text}' in error message: {err}"
@then('a ValidationError should be raised mentioning "{text}"')
def step_then_generic_validation_error_raised_mentioning(
context: Any, text: str
) -> None:
"""Shared @then step: check for a ValidationError stored in common context attrs."""
err = None
for attr in ("error", "lsp_error", "caught_error", "ontology_error"):
err = getattr(context, attr, None)
if err is not None:
break
assert err is not None, "Expected a ValidationError but none was raised"
assert text in str(err), f"Expected '{text}' in error message: {err}"
# ---------------------------------------------------------------------------
# validate_type
# ---------------------------------------------------------------------------
@when('I call validate_type with key "core.log.level" and value "INFO"')
def step_validate_type_same(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.level", "INFO")
@then('it should return "INFO" unchanged')
def step_verify_type_same(context: Any) -> None:
assert context.result == "INFO"
assert isinstance(context.result, str)
@when('I call validate_type with key "plan.concurrency" and string value "9090"')
def step_validate_type_str_to_int(context: Any) -> None:
context.result = ConfigService.validate_type("plan.concurrency", "9090")
@then("it should return the integer 9090")
def step_verify_int_coercion(context: Any) -> None:
assert context.result == 9090
assert isinstance(context.result, int)
@when(
'I call validate_type with key "plan.budget.warn-threshold" and string value "0.5"'
)
def step_validate_type_str_to_float(context: Any) -> None:
context.result = ConfigService.validate_type("plan.budget.warn-threshold", "0.5")
@then("it should return the float 0.5")
def step_verify_float_coercion(context: Any) -> None:
assert context.result == 0.5
assert isinstance(context.result, float)
@when('I call validate_type with key "core.log.level" and integer value 123')
def step_validate_type_int_to_str(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.level", 123)
@then('it should return the string "123"')
def step_verify_str_coercion(context: Any) -> None:
assert context.result == "123"
assert isinstance(context.result, str)
@when('I call validate_type with key "core.log.file-enabled" and string value "true"')
def step_validate_type_bool_true(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.file-enabled", "true")
@then("it should return boolean True")
def step_verify_bool_true(context: Any) -> None:
assert context.result is True
@when('I call validate_type with key "core.log.file-enabled" and string value "1"')
def step_validate_type_bool_one(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.file-enabled", "1")
@when('I call validate_type with key "core.log.file-enabled" and string value "yes"')
def step_validate_type_bool_yes(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.file-enabled", "yes")
@when('I call validate_type with key "core.log.file-enabled" and string value "false"')
def step_validate_type_bool_false(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.file-enabled", "false")
@then("it should return boolean False")
def step_verify_bool_false(context: Any) -> None:
assert context.result is False
@when('I call validate_type with key "core.log.file-enabled" and string value "0"')
def step_validate_type_bool_zero(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.file-enabled", "0")
@when('I call validate_type with key "core.log.file-enabled" and string value "no"')
def step_validate_type_bool_no(context: Any) -> None:
context.result = ConfigService.validate_type("core.log.file-enabled", "no")
@when('I call validate_type with key "core.log.file-enabled" and string value "maybe"')
def step_validate_type_bool_invalid(context: Any) -> None:
try:
context.result = ConfigService.validate_type("core.log.file-enabled", "maybe")
context.error = None
except TypeError as exc:
context.error = exc
@then('a TypeError should be raised mentioning "Cannot convert"')
def step_verify_bool_invalid_error(context: Any) -> None:
assert context.error is not None
assert isinstance(context.error, TypeError)
assert "Cannot convert" in str(context.error)
@when(
'I call validate_type with key "plan.concurrency" and string value "not_a_number"'
)
def step_validate_type_int_invalid(context: Any) -> None:
try:
context.result = ConfigService.validate_type("plan.concurrency", "not_a_number")
context.error = None
except TypeError as exc:
context.error = exc
@then('a TypeError should be raised mentioning "Type mismatch"')
def step_verify_type_mismatch_error(context: Any) -> None:
assert context.error is not None
assert isinstance(context.error, TypeError)
assert "Type mismatch" in str(context.error)
@when('I call validate_type with key "core.log.file-enabled" and a list value')
def step_validate_type_bool_list(context: Any) -> None:
try:
context.result = ConfigService.validate_type("core.log.file-enabled", [1, 2, 3])
context.error = None
except TypeError as exc:
context.error = exc
@when('I call validate_type with unknown key "fake.unknown" and value "x"')
def step_validate_type_unknown_key(context: Any) -> None:
try:
context.result = ConfigService.validate_type("fake.unknown", "x")
context.error = None
except ValueError as exc:
context.error = exc
@then('the raised ValueError should mention "Cannot validate type for unknown key"')
def step_verify_validate_type_unknown_key_error(context: Any) -> None:
assert context.error is not None
assert isinstance(context.error, ValueError)
assert "Cannot validate type for unknown key" in str(context.error)
# ---------------------------------------------------------------------------
# resolve - six-level chain
# ---------------------------------------------------------------------------
@given("a ConfigService with empty config for resolve tests")
def step_service_empty_for_resolve(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_path,
)
@when('I resolve key "core.log.level" with no overrides')
def step_resolve_no_overrides(context: Any) -> None:
context.result = context.svc.resolve("core.log.level")
@then('the resolved value should be "FATAL" from source DEFAULT')
def step_verify_resolve_default(context: Any) -> None:
assert context.result.value == "FATAL"
assert context.result.source == ConfigLevel.DEFAULT
@when('I resolve key "core.log.level" with cli_value "TRACE"')
def step_resolve_cli(context: Any) -> None:
context.result = context.svc.resolve("core.log.level", cli_value="TRACE")
@then('the resolved value should be "TRACE" from source CLI_FLAG')
def step_verify_resolve_cli(context: Any) -> None:
assert context.result.value == "TRACE"
assert context.result.source == ConfigLevel.CLI_FLAG
# Clean up env patch if present (from CLI-over-env scenario)
if hasattr(context, "_env_patch"):
context._env_patch.stop()
@given('the env var "{name}" is injected with "{value}"')
def step_inject_env_var(context: Any, name: str, value: str) -> None:
context._env_patch = patch.dict("os.environ", {name: value})
context._env_patch.start()
@then('the resolved value should be "WARNING" from source ENV_VAR')
def step_verify_resolve_env(context: Any) -> None:
assert context.result.value == "WARNING"
assert context.result.source == ConfigLevel.ENV_VAR
# Clean up the patch
if hasattr(context, "_env_patch"):
context._env_patch.stop()
@given(
'a ConfigService with project-scoped config for key "core.automation-profile" value "PROJECT_DEBUG" under project "myproj"'
)
def step_service_project_scoped(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
doc = tomlkit.document()
project_table = tomlkit.table()
myproj_table = tomlkit.table()
myproj_table["core.automation-profile"] = "PROJECT_DEBUG"
project_table["myproj"] = myproj_table
doc["project"] = project_table
with open(context.config_path, "w") as fh:
tomlkit.dump(doc, fh)
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_path,
)
@when('I resolve key "core.automation-profile" with project_name "myproj"')
def step_resolve_project(context: Any) -> None:
context.result = context.svc.resolve(
"core.automation-profile", project_name="myproj"
)
@then('the resolved value should be "PROJECT_DEBUG" from source PROJECT')
def step_verify_resolve_project(context: Any) -> None:
assert context.result.value == "PROJECT_DEBUG"
assert context.result.source == ConfigLevel.PROJECT
@given('a ConfigService with global config key "core.log.level" set to "GLOBAL_WARN"')
def step_service_global_config(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
doc = tomlkit.document()
doc["core.log.level"] = "GLOBAL_WARN"
with open(context.config_path, "w") as fh:
tomlkit.dump(doc, fh)
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_path,
)
@then('the resolved value should be "GLOBAL_WARN" from source GLOBAL')
def step_verify_resolve_global(context: Any) -> None:
assert context.result.value == "GLOBAL_WARN"
assert context.result.source == ConfigLevel.GLOBAL
@when('I resolve key "core.log.level" with verbose True and no overrides')
def step_resolve_verbose(context: Any) -> None:
context.result = context.svc.resolve("core.log.level", verbose=True)
@then("the resolved chain should have 6 entries covering all levels")
def step_verify_verbose_chain_length(context: Any) -> None:
chain = context.result.chain
assert len(chain) == 6, f"Expected 6 chain entries, got {len(chain)}"
sources = [entry["source"] for entry in chain]
assert sources == [
ConfigLevel.CLI_FLAG.value,
ConfigLevel.ENV_VAR.value,
ConfigLevel.LOCAL.value,
ConfigLevel.PROJECT.value,
ConfigLevel.GLOBAL.value,
ConfigLevel.DEFAULT.value,
]
@then(
'the verbose chain ENV_VAR entry should include env_name "CLEVERAGENTS_LOG_LEVEL"'
)
def step_verify_verbose_env_name(context: Any) -> None:
chain = context.result.chain
env_entry = next(e for e in chain if e["source"] == ConfigLevel.ENV_VAR.value)
assert env_entry["env_name"] == "CLEVERAGENTS_LOG_LEVEL"
@then("the verbose chain GLOBAL entry should include the config file path")
def step_verify_verbose_global_path(context: Any) -> None:
chain = context.result.chain
global_entry = next(e for e in chain if e["source"] == ConfigLevel.GLOBAL.value)
assert "path" in global_entry
assert str(context.config_path) in global_entry["path"]
# -- project scope skip for non-scopable keys --
@given(
'a ConfigService with project-scoped config for key "core.data-dir" value "/proj/data" under project "myproj"'
)
def step_service_project_scoped_nonscopable(context: Any) -> None:
context.config_dir.mkdir(parents=True, exist_ok=True)
doc = tomlkit.document()
project_table = tomlkit.table()
myproj_table = tomlkit.table()
myproj_table["core.data-dir"] = "/proj/data"
project_table["myproj"] = myproj_table
doc["project"] = project_table
with open(context.config_path, "w") as fh:
tomlkit.dump(doc, fh)
context.svc = ConfigService(
config_dir=context.config_dir,
config_path=context.config_path,
)
@when(
'I resolve key "core.automation-profile" with project_name "myproj" and verbose True'
)
def step_resolve_project_verbose(context: Any) -> None:
context.result = context.svc.resolve(
"core.automation-profile", project_name="myproj", verbose=True
)
@then('the verbose chain PROJECT entry should have value "PROJECT_DEBUG"')
def step_verify_verbose_project_value(context: Any) -> None:
chain = context.result.chain
proj_entry = next(e for e in chain if e["source"] == ConfigLevel.PROJECT.value)
assert proj_entry["value"] == "PROJECT_DEBUG"
@then('the verbose chain GLOBAL entry should have value "GLOBAL_WARN"')
def step_verify_verbose_global_value(context: Any) -> None:
chain = context.result.chain
global_entry = next(e for e in chain if e["source"] == ConfigLevel.GLOBAL.value)
assert global_entry["value"] == "GLOBAL_WARN"
@when('I resolve key "core.data-dir" with project_name "myproj"')
def step_resolve_nonscopable(context: Any) -> None:
context.result = context.svc.resolve("core.data-dir", project_name="myproj")
@then('the resolved value should not be "/proj/data"')
def step_verify_not_project_value(context: Any) -> None:
assert context.result.value != "/proj/data"
@then("the resolved source should be DEFAULT or GLOBAL")
def step_verify_source_not_project(context: Any) -> None:
assert context.result.source in (ConfigLevel.DEFAULT, ConfigLevel.GLOBAL)
# ---------------------------------------------------------------------------
# env_var_for_key
# ---------------------------------------------------------------------------
@when('I call env_var_for_key with "core.log.level"')
def step_call_env_var_for_key_valid(context: Any) -> None:
context.result = ConfigService.env_var_for_key("core.log.level")
@then('it should return "CLEVERAGENTS_LOG_LEVEL"')
def step_verify_env_var_for_key(context: Any) -> None:
assert context.result == "CLEVERAGENTS_LOG_LEVEL"
@when('I call env_var_for_key with "nonexistent.key"')
def step_call_env_var_for_key_unknown(context: Any) -> None:
try:
context.result = ConfigService.env_var_for_key("nonexistent.key")
context.error = None
except ValueError as exc:
context.error = exc
# The "a ValueError should be raised mentioning ..." @then step is defined
# above (step_then_generic_valueerror_raised_mentioning) and reused here.
# ---------------------------------------------------------------------------
# resolve_all
# ---------------------------------------------------------------------------
@when("I call resolve_all with no overrides")
def step_call_resolve_all(context: Any) -> None:
context.result = context.svc.resolve_all()
@then("the result should contain an entry for every registered key")
def step_verify_resolve_all_keys(context: Any) -> None:
for key in _REGISTRY:
assert key in context.result, f"Missing resolved key: {key}"
assert isinstance(context.result[key], ResolvedValue)
@when('I call resolve_all with cli_override "core.log.level" set to "FATAL"')
def step_call_resolve_all_with_overrides(context: Any) -> None:
context.result = context.svc.resolve_all(cli_overrides={"core.log.level": "FATAL"})
@then(
'the resolve_all result for "core.log.level" should have value "FATAL" and source CLI_FLAG'
)
def step_verify_resolve_all_override(context: Any) -> None:
rv = context.result["core.log.level"]
assert rv.value == "FATAL"
assert rv.source == ConfigLevel.CLI_FLAG