forked from HAL9000/cleveragents-core
31472b5413
Add Behave feature/step pairs that exercise uncovered branches across handlers, LSP, CLI, and service layers to reach the coverage gate. ISSUES CLOSED: #1232
221 lines
8.2 KiB
Python
221 lines
8.2 KiB
Python
"""Step definitions for config_cli_scope_coverage.feature (cfcov3).
|
|
|
|
Targets uncovered lines in cleveragents/cli/commands/config.py:
|
|
- Lines 224-229: invalid --scope triggers ValueError → typer.BadParameter
|
|
- Lines 252-253: --scope global reads previous from config_data
|
|
- Lines 254-255: --scope project reads previous from read_project_config()
|
|
- Lines 257-259: --scope local reads previous from read_local_config(),
|
|
then calls svc.set_value() and sets scope_display
|
|
|
|
All step text uses a 'cfcov3' prefix to avoid collisions with other
|
|
step definition files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.application.services.config_service import (
|
|
ConfigScope,
|
|
)
|
|
from cleveragents.cli.commands import config as config_mod
|
|
from cleveragents.cli.commands.config import app as config_app
|
|
|
|
_runner = CliRunner()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers — isolated temp directory
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _setup_cfcov3_temp(context: Context) -> None:
|
|
"""Redirect _CONFIG_DIR/_CONFIG_PATH to a fresh temp directory."""
|
|
if not hasattr(context, "_cfcov3_tmpdir"):
|
|
context._cfcov3_tmpdir = tempfile.mkdtemp(prefix="cfcov3_")
|
|
tmp = Path(context._cfcov3_tmpdir)
|
|
context._cfcov3_dir_patch = patch.object(config_mod, "_CONFIG_DIR", tmp)
|
|
context._cfcov3_path_patch = patch.object(
|
|
config_mod, "_CONFIG_PATH", tmp / "config.toml"
|
|
)
|
|
context._cfcov3_dir_patch.start()
|
|
context._cfcov3_path_patch.start()
|
|
|
|
context.add_cleanup(lambda: _teardown_cfcov3_temp(context))
|
|
|
|
|
|
def _teardown_cfcov3_temp(context: Context) -> None:
|
|
for attr in ("_cfcov3_dir_patch", "_cfcov3_path_patch"):
|
|
patcher = getattr(context, attr, None)
|
|
if patcher is not None:
|
|
with contextlib.suppress(RuntimeError):
|
|
patcher.stop()
|
|
tmpdir = getattr(context, "_cfcov3_tmpdir", None)
|
|
if tmpdir and os.path.isdir(tmpdir):
|
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
|
|
|
|
def _build_mock_svc(
|
|
config_data: dict[str, Any] | None = None,
|
|
project_config: dict[str, Any] | None = None,
|
|
local_config: dict[str, Any] | None = None,
|
|
) -> MagicMock:
|
|
"""Build a MagicMock that behaves like ConfigService for config_set."""
|
|
mock_svc = MagicMock()
|
|
mock_svc.read_config.return_value = config_data if config_data is not None else {}
|
|
mock_svc.read_project_config.return_value = (
|
|
project_config if project_config is not None else {}
|
|
)
|
|
mock_svc.read_local_config.return_value = (
|
|
local_config if local_config is not None else {}
|
|
)
|
|
|
|
# validate_type should pass through the value, coercing to int when possible
|
|
def _validate_type(key: str, value: Any) -> Any:
|
|
try:
|
|
return int(value)
|
|
except (ValueError, TypeError):
|
|
return value
|
|
|
|
mock_svc.validate_type.side_effect = _validate_type
|
|
mock_svc.set_value.return_value = None
|
|
mock_svc.write_config.return_value = None
|
|
|
|
# resolve is called by _validate_key indirectly through _get_service,
|
|
# but _validate_key uses _REGISTRY directly — no mock needed for that.
|
|
return mock_svc
|
|
|
|
|
|
# ===================================================================
|
|
# Background
|
|
# ===================================================================
|
|
|
|
|
|
@given("a cfcov3 isolated temp config directory")
|
|
def step_cfcov3_temp_dir(context: Context) -> None:
|
|
_setup_cfcov3_temp(context)
|
|
# Reset per-scenario mock tracking attributes
|
|
context.cfcov3_result = None
|
|
context.cfcov3_mock_svc = None
|
|
|
|
|
|
# ===================================================================
|
|
# Lines 224-229: invalid scope
|
|
# ===================================================================
|
|
|
|
|
|
@when('I invoke cfcov3 config set "{key}" "{value}" with scope "{scope}"')
|
|
def step_cfcov3_set_with_scope(
|
|
context: Context, key: str, value: str, scope: str
|
|
) -> None:
|
|
# For valid scopes, mock _get_service to avoid real file I/O.
|
|
# For invalid scopes, the ValueError is raised before _get_service
|
|
# calls read/write, but _get_service and validate_type are still
|
|
# called before the scope parsing — so we mock regardless.
|
|
mock_svc = context.cfcov3_mock_svc or _build_mock_svc()
|
|
context.cfcov3_mock_svc = mock_svc
|
|
|
|
patcher = patch.object(config_mod, "_get_service", return_value=mock_svc)
|
|
patcher.start()
|
|
context.add_cleanup(patcher.stop)
|
|
|
|
context.cfcov3_result = _runner.invoke(
|
|
config_app, ["set", key, value, "--scope", scope]
|
|
)
|
|
|
|
|
|
# ===================================================================
|
|
# Assertions: exit code and output content
|
|
# ===================================================================
|
|
|
|
|
|
@then("the cfcov3 CLI result should have a non-zero exit code")
|
|
def step_cfcov3_nonzero_exit(context: Context) -> None:
|
|
assert context.cfcov3_result.exit_code != 0, (
|
|
f"Expected non-zero exit, got {context.cfcov3_result.exit_code}: "
|
|
f"{context.cfcov3_result.output}"
|
|
)
|
|
|
|
|
|
@then("the cfcov3 CLI result should have exit code 0")
|
|
def step_cfcov3_zero_exit(context: Context) -> None:
|
|
assert context.cfcov3_result.exit_code == 0, (
|
|
f"Expected exit 0, got {context.cfcov3_result.exit_code}: "
|
|
f"{context.cfcov3_result.output}"
|
|
)
|
|
|
|
|
|
@then('the cfcov3 CLI output should contain "{text}"')
|
|
def step_cfcov3_output_contains(context: Context, text: str) -> None:
|
|
output = context.cfcov3_result.output
|
|
assert text in output, f"Expected '{text}' in output:\n{output}"
|
|
|
|
|
|
# ===================================================================
|
|
# Lines 252-253: --scope global with pre-existing value
|
|
# ===================================================================
|
|
|
|
|
|
@given('the cfcov3 global config has "{key}" set to {value:d}')
|
|
def step_cfcov3_global_preset(context: Context, key: str, value: int) -> None:
|
|
mock_svc = _build_mock_svc(config_data={key: value})
|
|
context.cfcov3_mock_svc = mock_svc
|
|
|
|
|
|
@then("the cfcov3 set_value mock should have been called with scope GLOBAL")
|
|
def step_cfcov3_set_value_global(context: Context) -> None:
|
|
mock_svc = context.cfcov3_mock_svc
|
|
assert mock_svc is not None, "No mock service was set up"
|
|
mock_svc.set_value.assert_called_once()
|
|
call_kwargs = mock_svc.set_value.call_args
|
|
# set_value(normalized, coerced, scope=config_scope)
|
|
assert call_kwargs.kwargs.get("scope") == ConfigScope.GLOBAL or (
|
|
len(call_kwargs.args) >= 3 and call_kwargs.args[2] == ConfigScope.GLOBAL
|
|
), f"Expected scope=GLOBAL in set_value call: {call_kwargs}"
|
|
|
|
|
|
# ===================================================================
|
|
# Lines 254-255: --scope project reads from read_project_config
|
|
# ===================================================================
|
|
|
|
|
|
@given('the cfcov3 project config returns "{key}" as {value:d}')
|
|
def step_cfcov3_project_preset(context: Context, key: str, value: int) -> None:
|
|
mock_svc = _build_mock_svc(project_config={key: value})
|
|
context.cfcov3_mock_svc = mock_svc
|
|
|
|
|
|
@then("the cfcov3 read_project_config mock should have been called")
|
|
def step_cfcov3_read_project_called(context: Context) -> None:
|
|
mock_svc = context.cfcov3_mock_svc
|
|
assert mock_svc is not None, "No mock service was set up"
|
|
mock_svc.read_project_config.assert_called_once()
|
|
|
|
|
|
# ===================================================================
|
|
# Lines 257-259: --scope local reads from read_local_config
|
|
# ===================================================================
|
|
|
|
|
|
@given('the cfcov3 local config returns "{key}" as {value:d}')
|
|
def step_cfcov3_local_preset(context: Context, key: str, value: int) -> None:
|
|
mock_svc = _build_mock_svc(local_config={key: value})
|
|
context.cfcov3_mock_svc = mock_svc
|
|
|
|
|
|
@then("the cfcov3 read_local_config mock should have been called")
|
|
def step_cfcov3_read_local_called(context: Context) -> None:
|
|
mock_svc = context.cfcov3_mock_svc
|
|
assert mock_svc is not None, "No mock service was set up"
|
|
mock_svc.read_local_config.assert_called_once()
|