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

281 lines
9.8 KiB
Python

"""Step definitions for the Config CLI feature."""
from __future__ import annotations
import os
import shutil
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import patch
from behave import given, then, when
from behave.runner import Context
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()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _patch_config_paths(context: Context) -> None:
"""Redirect config dir/path to a temp directory."""
if not hasattr(context, "_config_tmpdir"):
context._config_tmpdir = tempfile.mkdtemp()
tmp = Path(context._config_tmpdir)
context._config_dir_patch = patch.object(config_mod, "_CONFIG_DIR", tmp)
context._config_path_patch = patch.object(
config_mod, "_CONFIG_PATH", tmp / "config.toml"
)
context._config_dir_patch.start()
context._config_path_patch.start()
def _unpatch(context: Context) -> None:
"""Stop patches and clean up temp dir."""
for attr in ("_config_dir_patch", "_config_path_patch"):
patcher = getattr(context, attr, None)
if patcher is not None:
patcher.stop()
tmpdir = getattr(context, "_config_tmpdir", None)
if tmpdir and os.path.isdir(tmpdir):
shutil.rmtree(tmpdir, ignore_errors=True)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a clean config CLI test environment")
def step_clean_config_env(context: Context) -> None:
"""Set up an isolated temp config directory."""
_patch_config_paths(context)
# Register cleanup
if not hasattr(context, "_cleanup_handlers"):
context._cleanup_handlers: list[Any] = []
context._cleanup_handlers.append(lambda: _unpatch(context))
# ---------------------------------------------------------------------------
# SET
# ---------------------------------------------------------------------------
@when('I run config set "{key}" "{value}"')
def step_run_config_set(context: Context, key: str, value: str) -> None:
context.result = _runner.invoke(config_app, ["set", key, value])
@then("the config set should succeed")
def step_config_set_succeed(context: Context) -> None:
assert context.result.exit_code == 0, (
f"Expected exit 0, got {context.result.exit_code}: "
f"{context.result.output}{context.result.stderr}"
)
@then('the config set output should contain key "{key}"')
def step_config_set_contains_key(context: Context, key: str) -> None:
assert key in context.result.output, (
f"Expected '{key}' in output: {context.result.output}"
)
@then('the config set output should contain value "{value}"')
def step_config_set_contains_value(context: Context, value: str) -> None:
assert value in context.result.output, (
f"Expected '{value}' in output: {context.result.output}"
)
@then("the config set should fail with unknown key error")
def step_config_set_fail_unknown(context: Context) -> None:
assert context.result.exit_code != 0, (
f"Expected non-zero exit, got {context.result.exit_code}"
)
# ---------------------------------------------------------------------------
# GET
# ---------------------------------------------------------------------------
@when('I run config get "{key}"')
def step_run_config_get(context: Context, key: str) -> None:
context.result = _runner.invoke(config_app, ["get", key])
@when('I run config get key "{key}" formatted as "{fmt}"')
def step_run_config_get_fmt(context: Context, key: str, fmt: str) -> None:
context.result = _runner.invoke(config_app, ["get", key, "--format", fmt])
@then("the config get should succeed")
def step_config_get_succeed(context: Context) -> None:
assert context.result.exit_code == 0, (
f"Expected exit 0, got {context.result.exit_code}: {context.result.output}"
)
@then('the config get output should contain key "{key}"')
def step_config_get_contains_key(context: Context, key: str) -> None:
assert key in context.result.output, (
f"Expected '{key}' in output: {context.result.output}"
)
@then("the config get output should show the resolution chain")
def step_config_get_resolution_chain(context: Context) -> None:
output = context.result.output
assert "Resolution chain" in output or "resolution_chain" in output, (
f"Expected resolution chain in output: {output}"
)
@then("the config get should fail with unknown key error")
def step_config_get_fail_unknown(context: Context) -> None:
assert context.result.exit_code != 0, (
f"Expected non-zero exit, got {context.result.exit_code}"
)
# ---------------------------------------------------------------------------
# LIST
# ---------------------------------------------------------------------------
@when("I run config list")
def step_run_config_list(context: Context) -> None:
context.result = _runner.invoke(config_app, ["list"])
@when('I run config list "{pattern}"')
def step_run_config_list_filter(context: Context, pattern: str) -> None:
context.result = _runner.invoke(config_app, ["list", pattern])
@when('I run config list with --filter-values "{regex}"')
def step_run_config_list_filter_values(context: Context, regex: str) -> None:
context.result = _runner.invoke(config_app, ["list", "--filter-values", regex])
@when('I run config list with invalid regex "{regex}"')
def step_run_config_list_invalid_regex(context: Context, regex: str) -> None:
context.result = _runner.invoke(config_app, ["list", regex])
@when('I run config list with invalid value regex "{regex}"')
def step_run_config_list_invalid_val_regex(context: Context, regex: str) -> None:
context.result = _runner.invoke(config_app, ["list", "--filter-values", regex])
@when('I run config list with format "{fmt}"')
def step_run_config_list_format(context: Context, fmt: str) -> None:
context.result = _runner.invoke(config_app, ["list", "--format", fmt])
@when("I run config list with --show-secrets")
def step_run_config_list_show_secrets(context: Context) -> None:
context.result = _runner.invoke(config_app, ["list", "--show-secrets"])
@then("the config list should succeed")
def step_config_list_succeed(context: Context) -> None:
assert context.result.exit_code == 0, (
f"Expected exit 0, got {context.result.exit_code}: {context.result.output}"
)
@then("the config list output should contain multiple settings")
def step_config_list_multiple(context: Context) -> None:
# The rich table should have many rows; just check for known keys
assert "log_level" in context.result.output, (
f"Expected 'log_level' in output: {context.result.output}"
)
assert "server_port" in context.result.output, (
f"Expected 'server_port' in output: {context.result.output}"
)
@then('every listed key should match "{pattern}"')
def step_every_key_matches(context: Context, pattern: str) -> None:
import re
pat = re.compile(pattern)
# We check that all lines with key-like content match
# In table output, the key column is the leftmost data
output = context.result.output
# At minimum, no keys outside the filter should appear
assert pat.search(output), (
f"Expected at least one match for '{pattern}' in: {output}"
)
@then("the config list should fail with regex error")
def step_config_list_fail_regex(context: Context) -> None:
assert context.result.exit_code != 0, (
f"Expected non-zero exit, got {context.result.exit_code}"
)
# ---------------------------------------------------------------------------
# SECRET MASKING
# ---------------------------------------------------------------------------
@then("secret keys should show masked values")
def step_secrets_masked(context: Context) -> None:
output = context.result.output
# If any API key field is shown, it should be masked
if "api_key" in output or "token" in output:
assert "****" in output, f"Expected masked values (****) in output: {output}"
@then("secret keys should show actual values")
def step_secrets_shown(context: Context) -> None:
# With --show-secrets, the mask should not appear for fields with None
# Since defaults are None, this just checks the flag is accepted
assert context.result.exit_code == 0
# ---------------------------------------------------------------------------
# RESOLUTION CHAIN
# ---------------------------------------------------------------------------
@then('the resolution chain should include "{source}" source')
def step_resolution_chain_source(context: Context, source: str) -> None:
output = context.result.output
assert source in output, f"Expected '{source}' in resolution chain: {output}"
# ---------------------------------------------------------------------------
# ROUNDTRIP HELPERS
# ---------------------------------------------------------------------------
@given('I have set config "{key}" to "{value}"')
def step_preset_config(context: Context, key: str, value: str) -> None:
result = _runner.invoke(config_app, ["set", key, value])
assert result.exit_code == 0, f"Pre-set failed: {result.output}"
@then('the get value source should be "{source}"')
def step_get_source_is(context: Context, source: str) -> None:
output = context.result.output
assert source in output, f"Expected source '{source}' in: {output}"
# ---------------------------------------------------------------------------
# FORMAT CHECKS
# ---------------------------------------------------------------------------
# NOTE: "the output should be valid JSON" step is defined in
# cli_output_formats_steps.py and reused here.