29639558a3
Fix 8 distinct integration test and E2E failures:
1. JSON envelope unwrapping in test helpers
Helpers were asserting keys at the top-level of the JSON envelope
({command, status, exit_code, data, timing, messages}) rather than
inside the nested 'data' field. Updated helpers to access
parsed['data'] for assertions on the actual response payload:
- helper_cli_formats.py: action_list_json, action_show_yaml,
plan_list_json, global_format_json_version, global_format_yaml_version,
global_format_shorthand
- helper_automation_profile_cli.py: test_show_json, test_list_json
- helper_cli_extensions.py: action_show_json
- helper_config_cli.py: config_set_get_roundtrip
- helper_config_project_scope.py: cli_roundtrip
2. Plan list --namespace / -n option (issue #4301)
Add 'namespace' parameter to lifecycle_list_plans() in plan.py,
passing it through to service.list_plans(namespace=...) and
displaying it in the Filters panel.
3. Config registry key count (issue #4304)
Update expected count from 105 to 106 to match current registry
(additional server key was added via the server stubs feature).
4. Container tool exec mock fix (issue #4243)
The test mocked ev.resolve_and_validate (no longer called) instead
of ev.resolve_with_precedence. Fix the mock target so ToolRunner
correctly routes to the container path and returns the expected
error when no ContainerToolExecutor is configured.
5. E2E config CLI CLEVERAGENTS_HOME support
config.py used a hardcoded Path.home() / '.cleveragents' for the
config directory, ignoring CLEVERAGENTS_HOME. E2E tests run in
isolated temporary homes set via CLEVERAGENTS_HOME; this caused the
WF07 CI Profile Configuration test to read from the global config
(returning 'review') instead of the test-scoped config. Make
_get_config_dir() and _get_service() respect CLEVERAGENTS_HOME.
ISSUES CLOSED: #4204 #4205 #4206 #4243 #4301 #4302 #4303 #4304
175 lines
5.9 KiB
Python
175 lines
5.9 KiB
Python
"""Helper script for config_cli.robot smoke tests.
|
|
|
|
Each subcommand is a self-contained check that prints a sentinel on success.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
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)
|
|
|
|
# Suppress debug-level log output so that machine-readable CLI output (JSON/YAML)
|
|
# is not polluted by structlog debug messages from the plugin manager and other
|
|
# internal components that log at DEBUG level during initialisation.
|
|
from cleveragents.config.logging import configure_structlog # noqa: E402
|
|
|
|
configure_structlog(log_level="WARNING")
|
|
|
|
from typer.testing import CliRunner # noqa: E402
|
|
|
|
from cleveragents.cli.commands import config as config_mod # noqa: E402
|
|
from cleveragents.cli.commands.config import app as config_app # noqa: E402
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def _make_tmp_config() -> tuple[Path, Path]:
|
|
"""Create a temp config dir and return (dir, path)."""
|
|
tmpdir = Path(tempfile.mkdtemp())
|
|
return tmpdir, tmpdir / "config.toml"
|
|
|
|
|
|
def _run_with_tmp(args: list[str]) -> Any:
|
|
"""Run config CLI with a temporary config directory."""
|
|
tmpdir, tmppath = _make_tmp_config()
|
|
with (
|
|
patch.object(config_mod, "_CONFIG_DIR", tmpdir),
|
|
patch.object(config_mod, "_CONFIG_PATH", tmppath),
|
|
):
|
|
result = runner.invoke(config_app, args)
|
|
shutil.rmtree(str(tmpdir), ignore_errors=True)
|
|
return result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Subcommands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def config_list() -> None:
|
|
"""Verify config list outputs settings."""
|
|
result = _run_with_tmp(["list"])
|
|
if result.exit_code == 0 and "core.log.level" in result.output:
|
|
print("config-cli-list-ok")
|
|
else:
|
|
print(f"FAIL: list returned {result.exit_code}", file=sys.stderr)
|
|
print(result.output, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def config_list_json() -> None:
|
|
"""Verify config list --format json produces valid JSON."""
|
|
result = _run_with_tmp(["list", "--format", "json"])
|
|
if result.exit_code != 0:
|
|
print(f"FAIL: list json returned {result.exit_code}", file=sys.stderr)
|
|
print(result.output, file=sys.stderr)
|
|
sys.exit(1)
|
|
try:
|
|
json.loads(result.output)
|
|
print("config-cli-list-json-ok")
|
|
except json.JSONDecodeError as exc:
|
|
print(f"FAIL: invalid JSON: {exc}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def config_set_get_roundtrip() -> None:
|
|
"""Verify set then get roundtrip."""
|
|
tmpdir, tmppath = _make_tmp_config()
|
|
with (
|
|
patch.object(config_mod, "_CONFIG_DIR", tmpdir),
|
|
patch.object(config_mod, "_CONFIG_PATH", tmppath),
|
|
):
|
|
set_result = runner.invoke(config_app, ["set", "core.log.level", "DEBUG"])
|
|
if set_result.exit_code != 0:
|
|
print(f"FAIL: set returned {set_result.exit_code}", file=sys.stderr)
|
|
print(set_result.output, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
get_result = runner.invoke(
|
|
config_app, ["get", "core.log.level", "--format", "json"]
|
|
)
|
|
if get_result.exit_code != 0:
|
|
print(f"FAIL: get returned {get_result.exit_code}", file=sys.stderr)
|
|
print(get_result.output, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
envelope = json.loads(get_result.output)
|
|
# Output is wrapped in the spec-required envelope; config data is under "data"
|
|
data = (
|
|
envelope.get("data", envelope) if isinstance(envelope, dict) else envelope
|
|
)
|
|
if data.get("source") in ("config", "config_file", "global"):
|
|
print("config-cli-set-get-roundtrip-ok")
|
|
else:
|
|
print(f"FAIL: unexpected source {data.get('source')}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
shutil.rmtree(str(tmpdir), ignore_errors=True)
|
|
|
|
|
|
def config_secret_masking() -> None:
|
|
"""Verify secret values are masked in list output."""
|
|
result = _run_with_tmp(["list"])
|
|
if result.exit_code != 0:
|
|
print(f"FAIL: list returned {result.exit_code}", file=sys.stderr)
|
|
sys.exit(1)
|
|
# api_key fields should show ****
|
|
if "****" in result.output:
|
|
print("config-cli-secret-masking-ok")
|
|
else:
|
|
# All secrets are None by default, so no masking needed
|
|
# Check that secret fields exist but are not showing raw values
|
|
print("config-cli-secret-masking-ok")
|
|
|
|
|
|
def config_show_secrets() -> None:
|
|
"""Verify --show-secrets flag works."""
|
|
result = _run_with_tmp(["list", "--show-secrets"])
|
|
if result.exit_code == 0:
|
|
print("config-cli-show-secrets-ok")
|
|
else:
|
|
print(f"FAIL: list --show-secrets returned {result.exit_code}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def config_list_filter() -> None:
|
|
"""Verify config list with regex filter."""
|
|
result = _run_with_tmp(["list", "log.*"])
|
|
if result.exit_code == 0 and "core.log.level" in result.output:
|
|
print("config-cli-list-filter-ok")
|
|
else:
|
|
print(f"FAIL: list filter returned {result.exit_code}", file=sys.stderr)
|
|
print(result.output, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatcher
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COMMANDS: dict[str, Callable[[], None]] = {
|
|
"list": config_list,
|
|
"list-json": config_list_json,
|
|
"set-get-roundtrip": config_set_get_roundtrip,
|
|
"secret-masking": config_secret_masking,
|
|
"show-secrets": config_show_secrets,
|
|
"list-filter": config_list_filter,
|
|
}
|
|
|
|
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]]()
|