281 lines
9.8 KiB
Python
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.
|