"""Helper script for config_resolution.robot end-to-end tests. Each subcommand is a self-contained check that prints a sentinel on success. """ from __future__ import annotations import os import shutil import sys import tempfile from collections.abc import Callable from pathlib import Path from unittest.mock import patch # Ensure local source tree is importable _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from cleveragents.application.services.config_service import ( # noqa: E402 ConfigLevel, ConfigService, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_service() -> tuple[ConfigService, Path]: """Create a ConfigService backed by an isolated temp directory.""" tmpdir = Path(tempfile.mkdtemp()) svc = ConfigService( config_dir=tmpdir, config_path=tmpdir / "config.toml", project_root=None, ) return svc, tmpdir def _cleanup(tmpdir: Path) -> None: """Remove the temp directory ignoring errors.""" shutil.rmtree(str(tmpdir), ignore_errors=True) # --------------------------------------------------------------------------- # Subcommands # --------------------------------------------------------------------------- def registry_key_count() -> None: """Verify that the registry contains the expected number of keys. The count is updated as new spec-defined keys are added. Current: 106 keys (was 105; 1 new key added in recent spec update). """ registry = ConfigService.registry() count = len(registry) if count >= 105: print("config-registry-key-count-ok") else: print(f"FAIL: expected >= 105 keys, got {count}", file=sys.stderr) sys.exit(1) def resolve_default() -> None: """Verify default resolution with no overrides.""" svc, tmpdir = _make_service() try: result = svc.resolve("core.log.level") if result.value == "FATAL" and result.source == ConfigLevel.DEFAULT: print("config-resolution-default-ok") else: print( f"FAIL: expected FATAL/default, got {result.value}/{result.source}", file=sys.stderr, ) sys.exit(1) finally: _cleanup(tmpdir) def resolve_global() -> None: """Verify that a TOML file value overrides the default.""" svc, tmpdir = _make_service() try: svc.set_value("core.log.level", "DEBUG") result = svc.resolve("core.log.level") if result.value == "DEBUG" and result.source == ConfigLevel.GLOBAL: print("config-resolution-global-ok") else: print( f"FAIL: expected DEBUG/global, got {result.value}/{result.source}", file=sys.stderr, ) sys.exit(1) finally: _cleanup(tmpdir) def resolve_env() -> None: """Verify that env var overrides the global config value.""" svc, tmpdir = _make_service() env_key = "CLEVERAGENTS_LOG_LEVEL" old_val = os.environ.get(env_key) try: # Set a global value first so we can prove env wins svc.set_value("core.log.level", "DEBUG") os.environ[env_key] = "WARNING" result = svc.resolve("core.log.level") if result.value == "WARNING" and result.source == ConfigLevel.ENV_VAR: print("config-resolution-env-ok") else: print( f"FAIL: expected WARNING/env_var, got {result.value}/{result.source}", file=sys.stderr, ) sys.exit(1) finally: if old_val is None: os.environ.pop(env_key, None) else: os.environ[env_key] = old_val _cleanup(tmpdir) def resolve_cli() -> None: """Verify that cli_value has the highest priority.""" svc, tmpdir = _make_service() env_key = "CLEVERAGENTS_LOG_LEVEL" old_val = os.environ.get(env_key) try: # Set all lower levels so we can prove CLI wins svc.set_value("core.log.level", "DEBUG") os.environ[env_key] = "WARNING" result = svc.resolve("core.log.level", cli_value="ERROR") if result.value == "ERROR" and result.source == ConfigLevel.CLI_FLAG: print("config-resolution-cli-ok") else: print( f"FAIL: expected ERROR/cli_flag, got {result.value}/{result.source}", file=sys.stderr, ) sys.exit(1) finally: if old_val is None: os.environ.pop(env_key, None) else: os.environ[env_key] = old_val _cleanup(tmpdir) def resolve_project() -> None: """Verify project-scoped config resolution via project_name.""" svc, tmpdir = _make_service() try: # Write project-scoped override into the TOML file import tomlkit doc = tomlkit.document() project_table = tomlkit.table() myproj_table = tomlkit.table() myproj_table["core.automation-profile"] = "full-auto" project_table["myproject"] = myproj_table doc["project"] = project_table svc._config_dir.mkdir(parents=True, exist_ok=True) with open(svc._config_path, "w") as fh: tomlkit.dump(doc, fh) result = svc.resolve("core.automation-profile", project_name="myproject") if result.value == "full-auto" and result.source == ConfigLevel.PROJECT: print("config-resolution-project-ok") else: print( f"FAIL: expected full-auto/project, got {result.value}/{result.source}", file=sys.stderr, ) sys.exit(1) finally: _cleanup(tmpdir) def resolve_verbose_chain() -> None: """Verify verbose=True populates all 6 chain entries.""" svc, tmpdir = _make_service() try: result = svc.resolve("core.log.level", verbose=True) chain_len = len(result.chain) if chain_len == 6: # Verify the sources in order expected_sources = [ ConfigLevel.CLI_FLAG.value, ConfigLevel.ENV_VAR.value, ConfigLevel.LOCAL.value, ConfigLevel.PROJECT.value, ConfigLevel.GLOBAL.value, ConfigLevel.DEFAULT.value, ] actual_sources = [entry["source"] for entry in result.chain] if actual_sources == expected_sources: print("config-resolution-verbose-chain-ok") else: print( f"FAIL: chain sources mismatch: {actual_sources}", file=sys.stderr, ) sys.exit(1) else: print( f"FAIL: expected 6 chain entries, got {chain_len}", file=sys.stderr, ) sys.exit(1) finally: _cleanup(tmpdir) def env_var_provider() -> None: """Verify that provider.openai.api-key maps to OPENAI_API_KEY.""" env_name = ConfigService.env_var_for_key("provider.openai.api-key") if env_name == "OPENAI_API_KEY": print("config-env-var-provider-ok") else: print( f"FAIL: expected OPENAI_API_KEY, got {env_name}", file=sys.stderr, ) sys.exit(1) def type_coercion() -> None: """Verify str→int, str→float, and str→bool coercion.""" errors: list[str] = [] # str → int result_int = ConfigService.validate_type("core.log.retention-days", "45") if result_int != 45 or not isinstance(result_int, int): errors.append( f"int coercion: expected 45 (int), got {result_int!r} " f"({type(result_int).__name__})" ) # str → float result_float = ConfigService.validate_type("plan.budget.per-plan", "3.14") if result_float != 3.14 or not isinstance(result_float, float): errors.append( f"float coercion: expected 3.14 (float), got {result_float!r} " f"({type(result_float).__name__})" ) # str → bool (true) result_bool_t = ConfigService.validate_type("core.log.file-enabled", "true") if result_bool_t is not True: errors.append(f"bool coercion (true): got {result_bool_t!r}") # str → bool (false) result_bool_f = ConfigService.validate_type("core.log.file-enabled", "false") if result_bool_f is not False: errors.append(f"bool coercion (false): got {result_bool_f!r}") if errors: for err in errors: print(f"FAIL: {err}", file=sys.stderr) sys.exit(1) else: print("config-type-coercion-ok") def cli_list_integration() -> None: """Verify ``agents config list`` outputs all registered keys.""" from typer.testing import CliRunner from cleveragents.cli.commands import config as config_mod from cleveragents.cli.commands.config import app as config_app runner = CliRunner() tmpdir = Path(tempfile.mkdtemp()) tmppath = tmpdir / "config.toml" try: with ( patch.object(config_mod, "_CONFIG_DIR", tmpdir), patch.object(config_mod, "_CONFIG_PATH", tmppath), ): result = runner.invoke(config_app, ["list"]) if result.exit_code != 0: print(f"FAIL: config list exited {result.exit_code}", file=sys.stderr) print(result.output, file=sys.stderr) sys.exit(1) output = result.output # Spot-check several representative keys from different sections missing: list[str] = [] for key_fragment in ["log.level", "automation-profile", "openai.api-key"]: if key_fragment not in output: missing.append(key_fragment) if missing: print( f"FAIL: missing key fragments in output: {missing}", file=sys.stderr, ) sys.exit(1) print("config-cli-list-integration-ok") finally: _cleanup(tmpdir) # --------------------------------------------------------------------------- # Dispatcher # --------------------------------------------------------------------------- _COMMANDS: dict[str, Callable[[], None]] = { "registry-key-count": registry_key_count, "resolve-default": resolve_default, "resolve-global": resolve_global, "resolve-env": resolve_env, "resolve-cli": resolve_cli, "resolve-project": resolve_project, "resolve-verbose-chain": resolve_verbose_chain, "env-var-provider": env_var_provider, "type-coercion": type_coercion, "cli-list-integration": cli_list_integration, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr) sys.exit(2) _COMMANDS[sys.argv[1]]()