b4a514e0f4
CI / benchmark-regression (push) Failing after 34s
CI / tdd_quality_gate (push) Has been skipped
CI / push-validation (push) Successful in 1m5s
CI / helm (push) Successful in 1m6s
CI / build (push) Successful in 1m23s
CI / lint (push) Successful in 1m55s
CI / typecheck (push) Successful in 2m4s
CI / quality (push) Successful in 2m2s
CI / security (push) Successful in 2m2s
CI / integration_tests (push) Successful in 7m35s
CI / e2e_tests (push) Successful in 4m30s
CI / unit_tests (push) Successful in 9m19s
CI / docker (push) Successful in 1m47s
CI / coverage (push) Successful in 12m33s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Has been cancelled
Implements ADR-021 §Global CLI Flags and ADR-024 §Resolution Chain.
All three global options were missing from main_callback(), causing any
invocation with these flags to crash with 'No such option'.
Changes:
- src/cleveragents/cli/main.py
* Add _VERBOSITY_LOG_LEVELS tuple mapping verbose count to log levels
(0=CRITICAL/silent, 1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG, 5+=DEBUG)
* Add --data-dir PATH option: validates path is a directory if it exists,
sets CLEVERAGENTS_DATA_DIR env var, resets Settings singleton
* Add --config-path PATH option: validates file exists and is a file,
sets CLEVERAGENTS_CONFIG_PATH env var for ConfigService to pick up
* Add -v (count=True) option: wires verbose count to configure_structlog
* Store all three values in ctx.obj for subcommand access
* Update _print_basic_help() to include the three new global options so
'cleveragents --help' (fast path) also lists them
* In the catch-all Exception handler, print the original exception
type+message directly to err_console so actionable details remain
visible even when log level is CRITICAL (e.g. 'No such option')
- src/cleveragents/application/services/config_service.py
* ConfigService.__init__ now checks CLEVERAGENTS_CONFIG_PATH env var
when no explicit config_path is passed, completing the resolution
chain for --config-path CLI override
Tests (all passing):
- features/cli_global_options.feature (21 Behave scenarios)
- features/steps/cli_global_options_steps.py
- robot/cli_global_options.robot (8 Robot Framework integration tests)
- robot/helper_cli_global_options.py
Quality gates: lint ✓ typecheck ✓ unit_tests ✓ integration_tests ✓
Coverage: 96.52% (threshold 96.5% ✓)
ISSUES CLOSED: #6785
205 lines
6.7 KiB
Python
205 lines
6.7 KiB
Python
"""Helper script for cli_global_options.robot integration tests.
|
|
|
|
Each sub-command verifies one end-to-end behaviour of the new global CLI
|
|
options (--data-dir, --config-path, -v) introduced by issue #6785.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _invoke(args: list[str]) -> tuple[int, str]:
|
|
"""Invoke the Typer app and return (exit_code, combined_output)."""
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.main import app
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(app, args, catch_exceptions=True)
|
|
output = result.output or ""
|
|
return result.exit_code, output
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test cases (sub-commands)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_data_dir_accepted() -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, output = _invoke(["--data-dir", tmp, "version"])
|
|
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
|
|
print("cli-global-options-data-dir-accepted-ok")
|
|
|
|
|
|
def test_config_path_accepted() -> None:
|
|
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as tmp:
|
|
tmp.write(b"# test config\n")
|
|
tmp_path = tmp.name
|
|
try:
|
|
code, output = _invoke(["--config-path", tmp_path, "version"])
|
|
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
|
|
print("cli-global-options-config-path-accepted-ok")
|
|
finally:
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
def test_verbosity_accepted() -> None:
|
|
code, output = _invoke(["-v", "version"])
|
|
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
|
|
print("cli-global-options-verbosity-accepted-ok")
|
|
|
|
|
|
def test_combined_options() -> None:
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as tmp_cfg:
|
|
tmp_cfg.write(b"# test\n")
|
|
tmp_cfg_path = tmp_cfg.name
|
|
try:
|
|
code, output = _invoke(
|
|
[
|
|
"--data-dir",
|
|
tmp_dir,
|
|
"--config-path",
|
|
tmp_cfg_path,
|
|
"version",
|
|
]
|
|
)
|
|
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
|
|
print("cli-global-options-combined-ok")
|
|
finally:
|
|
os.unlink(tmp_cfg_path)
|
|
|
|
|
|
def test_data_dir_override() -> None:
|
|
"""Verify --data-dir sets CLEVERAGENTS_DATA_DIR env var during invocation."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
code, output = _invoke(["--data-dir", tmp, "version"])
|
|
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
|
|
|
|
# Verify the env var was set to (or derived from) the tmp dir
|
|
env_val = os.environ.get("CLEVERAGENTS_DATA_DIR")
|
|
if env_val:
|
|
resolved_env = str(Path(env_val).resolve())
|
|
resolved_tmp = str(Path(tmp).resolve())
|
|
assert resolved_env == resolved_tmp, (
|
|
f"CLEVERAGENTS_DATA_DIR={env_val!r} != {tmp!r}"
|
|
)
|
|
|
|
print("cli-global-options-data-dir-override-ok")
|
|
|
|
|
|
def test_config_path_override() -> None:
|
|
"""Verify --config-path sets CLEVERAGENTS_CONFIG_PATH env var during invocation."""
|
|
with tempfile.NamedTemporaryFile(suffix=".toml", delete=False) as tmp:
|
|
tmp.write(b"# test\n")
|
|
tmp_path = tmp.name
|
|
try:
|
|
code, output = _invoke(["--config-path", tmp_path, "version"])
|
|
assert code == 0, f"Expected exit 0, got {code}. Output: {output}"
|
|
env_val = os.environ.get("CLEVERAGENTS_CONFIG_PATH")
|
|
if env_val:
|
|
assert str(Path(env_val).resolve()) == str(Path(tmp_path).resolve()), (
|
|
f"CLEVERAGENTS_CONFIG_PATH={env_val!r} != {tmp_path!r}"
|
|
)
|
|
print("cli-global-options-config-path-override-ok")
|
|
finally:
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
def test_verbosity_levels() -> None:
|
|
"""Verify the verbosity count → log level mapping."""
|
|
from unittest.mock import MagicMock
|
|
|
|
from cleveragents.cli.formatting import OutputFormat
|
|
from cleveragents.cli.main import main_callback
|
|
|
|
expected_levels = {
|
|
0: "CRITICAL",
|
|
1: "ERROR",
|
|
2: "WARNING",
|
|
3: "INFO",
|
|
4: "DEBUG",
|
|
5: "DEBUG",
|
|
}
|
|
|
|
for count, expected in expected_levels.items():
|
|
captured_levels: list[str] = []
|
|
|
|
def _make_capture(levels: list[str]): # type: ignore[no-untyped-def]
|
|
def _capture(*, env: str = "development", log_level: str = "INFO") -> None:
|
|
levels.append(log_level)
|
|
|
|
return _capture
|
|
|
|
mock_ctx = MagicMock()
|
|
mock_ctx.obj = {}
|
|
|
|
with patch(
|
|
"cleveragents.config.logging.configure_structlog",
|
|
side_effect=_make_capture(captured_levels),
|
|
):
|
|
main_callback(
|
|
ctx=mock_ctx,
|
|
version=None, # type: ignore[arg-type]
|
|
show_secrets=False,
|
|
fmt=OutputFormat.RICH,
|
|
data_dir=None,
|
|
config_path=None,
|
|
verbose=count,
|
|
)
|
|
|
|
assert captured_levels, f"configure_structlog not called for count={count}"
|
|
actual = captured_levels[-1].upper()
|
|
assert actual == expected, (
|
|
f"verbose={count}: expected log_level={expected!r}, got {actual!r}"
|
|
)
|
|
|
|
print("cli-global-options-verbosity-levels-ok")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_COMMANDS: dict[str, object] = {
|
|
"data-dir-accepted": test_data_dir_accepted,
|
|
"config-path-accepted": test_config_path_accepted,
|
|
"verbosity-accepted": test_verbosity_accepted,
|
|
"combined-options": test_combined_options,
|
|
"data-dir-override": test_data_dir_override,
|
|
"config-path-override": test_config_path_override,
|
|
"verbosity-levels": test_verbosity_levels,
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print("Usage: helper_cli_global_options.py <command>", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
cmd = sys.argv[1]
|
|
fn = _COMMANDS.get(cmd)
|
|
if fn is None:
|
|
print(f"Unknown command: {cmd}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
fn() # type: ignore[call-arg]
|
|
except Exception as exc:
|
|
print(f"FAILED: {exc}", file=sys.stderr)
|
|
import traceback
|
|
|
|
traceback.print_exc(file=sys.stderr)
|
|
sys.exit(1)
|