Files
cleveragents-core/robot/helper_cli_global_options.py
hurui200320 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
fix(cli): add spec-required --data-dir, --config-path, and -v global options to main_callback
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
2026-05-12 01:05:10 +00:00

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)