Files
cleveragents-core/features/steps/project_commands_coverage_steps.py
freemo 4f2aa4189c
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 3m20s
CI / typecheck (pull_request) Successful in 3m56s
CI / build (pull_request) Successful in 37s
CI / security (pull_request) Successful in 4m2s
CI / quality (pull_request) Successful in 4m8s
CI / unit_tests (pull_request) Failing after 4m13s
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 7m10s
CI / e2e_tests (pull_request) Successful in 9m34s
CI / coverage (pull_request) Successful in 11m1s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-regression (pull_request) Has been cancelled
fix(test): guard result.stderr access against ValueError
Click/Typer CliRunner.Result.stderr is a property that raises
ValueError when stderr was not separately captured (mix_stderr=True
is the default).  Wrap all result.stderr accesses in try/except
to handle this gracefully.
2026-03-22 05:01:06 +00:00

707 lines
24 KiB
Python

"""Step definitions for project commands coverage tests."""
from __future__ import annotations
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from typer.testing import CliRunner
from cleveragents.application.services.context_service import DEFAULT_IGNORE_PATTERNS
from cleveragents.cli.commands import project
from cleveragents.core.exceptions import (
CleverAgentsError,
ConfigurationError,
ValidationError,
)
@given("I have a temporary working directory")
def step_temp_working_directory(context):
"""Create a temporary working directory."""
context.temp_dir = tempfile.mkdtemp()
context.original_cwd = Path.cwd()
import os
os.chdir(context.temp_dir)
@given('I create a subdirectory "{dirname}"')
def step_create_subdirectory(context, dirname):
"""Create a subdirectory in the temp directory."""
subdir = Path(context.temp_dir) / dirname
subdir.mkdir(parents=True, exist_ok=True)
context.subdirectory = subdir
@given("I have already initialized a project")
def step_already_initialized(context):
"""Simulate an already initialized project."""
project_dir = Path(context.temp_dir) / ".cleveragents"
project_dir.mkdir(exist_ok=True)
context.existing_project = True
@given("I have an initialized project with stats")
def step_initialized_with_stats(context):
"""Mock an initialized project with statistics."""
context.mock_project = MagicMock()
context.mock_project.name = "test-project"
context.mock_project.path = Path(context.temp_dir)
context.mock_project.created_at = "2024-01-01 10:00:00"
context.mock_stats = {
"plans": 5,
"context_files": 10,
"changes": 25,
"current_plan": "plan-001",
}
@given("there is no initialized project")
def step_no_project(context):
"""Ensure no project exists."""
context.no_project = True
@given("the project service will raise a ValidationError with details")
def step_mock_validation_error(context):
"""Mock project service to raise ValidationError."""
context.mock_error = ValidationError(
"Invalid project name",
details={"name": "contains invalid characters", "path": "does not exist"},
)
@given("the project service will raise a ConfigurationError")
def step_mock_configuration_error(context):
"""Mock project service to raise ConfigurationError."""
context.mock_error = ConfigurationError("Missing required configuration")
@given("the project service will raise a CleverAgentsError")
def step_mock_cleveragents_error(context):
"""Mock project service to raise CleverAgentsError."""
context.mock_error = CleverAgentsError("Generic project error")
@given("the project service will raise an unexpected exception")
def step_mock_unexpected_error(context):
"""Mock project service to raise unexpected exception."""
context.mock_error = Exception("Unexpected error occurred")
@given("the project service will raise a CleverAgentsError on status")
def step_mock_status_error(context):
"""Mock project service to raise error on status."""
context.mock_status_error = CleverAgentsError("Cannot get project status")
@when("I run project init without any parameters")
def step_run_project_init_default(context):
"""Run project init with default parameters."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_project = MagicMock()
mock_project.name = Path.cwd().name
mock_project.path = Path.cwd()
mock_service.initialize_project.return_value = mock_project
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init"])
context.result = result
context.mock_service = mock_service
@when('I run project init with name "{name}"')
def step_run_project_init_with_name(context, name):
"""Run project init with custom name."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_project = MagicMock()
mock_project.name = name
mock_project.path = Path.cwd()
mock_service.initialize_project.return_value = mock_project
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init", name])
context.result = result
context.project_name = name
@when('I run project init with path "{path}"')
def step_run_project_init_with_path(context, path):
"""Run project init with custom path."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_project = MagicMock()
project_path = Path(context.temp_dir) / path
mock_project.name = Path(path).name
mock_project.path = project_path
mock_service.initialize_project.return_value = mock_project
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init", "--path", str(project_path)])
context.result = result
context.project_path = project_path
@when("I execute project init with force option")
def step_execute_project_init_force(context):
"""Run project init with force flag."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_project = MagicMock()
mock_project.name = "forced-project"
mock_project.path = Path.cwd()
mock_service.initialize_project.return_value = mock_project
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init", "--force"])
context.result = result
@when("I run project init and it fails with validation error")
def step_run_project_init_validation_error(context):
"""Run project init that fails with ValidationError."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.initialize_project.side_effect = context.mock_error
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init"])
context.result = result
@when("I run project init and it fails with configuration error")
def step_run_project_init_config_error(context):
"""Run project init that fails with ConfigurationError."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.initialize_project.side_effect = context.mock_error
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init"])
context.result = result
@when("I run project init and it fails with CleverAgentsError")
def step_run_project_init_cleveragents_error(context):
"""Run project init that fails with CleverAgentsError."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.initialize_project.side_effect = context.mock_error
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init"])
context.result = result
@when("I run project init and it fails unexpectedly")
def step_run_project_init_unexpected_error(context):
"""Run project init that fails with unexpected error."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.initialize_project.side_effect = context.mock_error
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["init"])
context.result = result
@when("I execute project status command")
def step_execute_project_status(context):
"""Run project status command."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
if hasattr(context, "no_project") and context.no_project:
mock_service.get_current_project.return_value = None
elif hasattr(context, "mock_status_error"):
mock_service.get_current_project.side_effect = context.mock_status_error
else:
mock_service.get_current_project.return_value = context.mock_project
mock_service.get_project_stats.return_value = context.mock_stats
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["status"])
context.result = result
@when("I execute project status and it fails")
def step_execute_project_status_error(context):
"""Run project status that fails."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.get_current_project.side_effect = context.mock_status_error
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["status"])
context.result = result
@when("I run project list command")
def step_run_project_list(context):
"""Run project list command.
Mocks the project repository to return an empty list so the test
is fully isolated from database state left by prior scenarios
(which caused flaky failures in sequential / coverage mode).
"""
from unittest.mock import MagicMock, patch
mock_repo = MagicMock()
mock_repo.list_projects.return_value = []
runner = CliRunner()
with patch(
"cleveragents.cli.commands.project._get_namespaced_project_repo",
return_value=mock_repo,
):
result = runner.invoke(project.app, ["list"])
context.result = result
@when("I run project clean command")
def step_run_project_clean(context):
"""Run project clean command."""
runner = CliRunner()
result = runner.invoke(project.app, ["clean"])
context.result = result
@when("I run project clean command with --yes flag")
def step_run_project_clean_yes(context):
"""Run project clean with confirmation."""
runner = CliRunner()
result = runner.invoke(project.app, ["clean", "--yes"])
context.result = result
@then("the project should be initialized in current directory")
def step_check_project_initialized(context):
"""Check project was initialized."""
assert context.result.exit_code == 0
assert "initialized successfully" in context.result.output
@then("the success message should show project name and location")
def step_check_success_message(context):
"""Check success message content."""
assert "Project" in context.result.output
assert "Location:" in context.result.output
@then("the .cleveragents directory should be created")
def step_check_cleveragents_dir(context):
"""Check .cleveragents directory creation."""
# This is mocked, but we check the service was called correctly
assert context.mock_service.initialize_project.called
@then('the project should be initialized with name "{name}"')
def step_check_project_name(context, name):
"""Check project initialized with correct name."""
assert context.result.exit_code == 0
assert name in context.result.output
@then('the success panel should display "{name}"')
def step_check_panel_name(context, name):
"""Check success panel displays name."""
assert name in context.result.output
@then('the project should be initialized in "{dirname}" directory')
def step_check_project_in_dir(context, dirname):
"""Check project initialized in correct directory."""
assert context.result.exit_code == 0
@then('the .cleveragents directory should exist in "{dirname}"')
def step_check_cleveragents_in_dir(context, dirname):
"""Check .cleveragents exists in specified directory."""
# This is mocked, just verify the call happened
assert context.result.exit_code == 0
@then("the project should be reinitialized successfully")
def step_check_reinitialized(context):
"""Check project was reinitialized."""
assert context.result.exit_code == 0
assert "initialized successfully" in context.result.output
@then("no error should be raised")
def step_check_no_error(context):
"""Check no error was raised."""
assert context.result.exit_code == 0
@then("the error message should be displayed in red")
def step_check_error_red(context):
"""Check error displayed in red."""
assert "Validation Error" in context.result.output
@then("the validation error details should be shown")
def step_check_validation_details(context):
"""Check validation error details shown."""
assert "name:" in context.result.output or "path:" in context.result.output
@then("the command should abort with ValidationError")
def step_check_abort_validation(context):
"""Check command aborted with validation error."""
assert context.result.exit_code != 0
@then("the configuration error message should be displayed")
def step_check_config_error(context):
"""Check configuration error displayed."""
assert "Configuration Error" in context.result.output
@then("the command should abort with ConfigurationError")
def step_check_abort_config(context):
"""Check command aborted with config error."""
assert context.result.exit_code != 0
@then("the generic error message should be displayed")
def step_check_generic_error(context):
"""Check generic error displayed."""
assert "Error:" in context.result.output
@then("the command should abort with CleverAgentsError")
def step_check_abort_cleveragents(context):
"""Check command aborted with CleverAgentsError."""
assert context.result.exit_code != 0
@then("the unexpected error message should be displayed")
def step_check_unexpected_error(context):
"""Check unexpected error displayed."""
assert "Unexpected error" in context.result.output
@then("the command should abort with generic exception")
def step_check_abort_generic(context):
"""Check command aborted with generic exception."""
assert context.result.exit_code != 0
@then("the project information should be displayed")
def step_check_project_info(context):
"""Check project info displayed."""
assert "Project Status" in context.result.output
assert "test-project" in context.result.output
@then("the statistics should include plans count")
def step_check_plans_count(context):
"""Check plans count in statistics."""
assert "Plans: 5" in context.result.output
@then("the statistics should include context files count")
def step_check_context_files(context):
"""Check context files count."""
assert "Context Files: 10" in context.result.output
@then("the statistics should include changes count")
def step_check_changes_count(context):
"""Check changes count."""
assert "Total Changes: 25" in context.result.output
@then("the current plan should be shown")
def step_check_current_plan(context):
"""Check current plan shown."""
assert "Current Plan: plan-001" in context.result.output
@then('an error should show "{message}"')
def step_check_error_message(context, message):
"""Check specific error message."""
assert message in context.result.output
@then('it should suggest running "{command}"')
def step_check_suggestion(context, command):
"""Check command suggestion."""
assert command in context.result.output
@then("the error message should be shown")
def step_check_error_shown(context):
"""Check error message shown."""
assert "Error:" in context.result.output
@then("the command should abort")
def step_check_abort(context):
"""Check command aborted."""
assert context.result.exit_code != 0
@then('it should display "{message}"')
def step_check_display_message(context, message):
"""Check specific message displayed."""
assert message in context.result.output
@then("the command should abort regardless of confirmation")
def step_check_abort_regardless(context):
"""Check command aborts even with confirmation."""
assert context.result.exit_code != 0
assert "not yet implemented" in context.result.output
@given("I have a mocked project with file filters")
def step_mock_project_filters(context):
"""Prepare a mocked project for file filter commands."""
context.mock_project = MagicMock()
context.mock_project.name = "filters-project"
context.mock_project.path = Path(getattr(context, "temp_dir", Path.cwd()))
context.project_includes = ["src/*"]
context.project_excludes = ["tests/*"]
@when("I run project file-filter show")
def step_run_file_filter_show(context):
"""Run the file-filter show command."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.get_current_project.return_value = context.mock_project
mock_service.get_project_filters.return_value = (
getattr(context, "project_includes", []),
getattr(context, "project_excludes", []),
)
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["file-filter", "show"])
context.result = result
context.mock_service = mock_service
@when("I run project file-filter show with no project")
def step_run_file_filter_show_no_project(context):
"""Run the file-filter show command without an active project."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.get_current_project.return_value = None
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["file-filter", "show"])
context.result = result
@when(
'I add file filters include "{include_glob}" exclude "{exclude_glob}" with defaults'
)
def step_add_file_filters_with_defaults(context, include_glob, exclude_glob):
"""Run the file-filter add command with include, exclude, and defaults."""
runner = CliRunner()
expected_excludes = [exclude_glob, *DEFAULT_IGNORE_PATTERNS]
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.get_current_project.return_value = context.mock_project
mock_service.update_file_filters.return_value = context.mock_project
mock_service.get_project_filters.return_value = (
[include_glob],
expected_excludes,
)
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(
project.app,
[
"file-filter",
"add",
"--include",
include_glob,
"--exclude",
exclude_glob,
"--defaults",
],
)
context.result = result
context.mock_service = mock_service
context.expected_include = include_glob
context.expected_excludes = expected_excludes
@when("I clear project file filters without flags")
def step_clear_file_filters(context):
"""Run the file-filter clear command without specifying flags."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.get_current_project.return_value = context.mock_project
mock_service.update_file_filters.return_value = context.mock_project
mock_service.get_project_filters.return_value = ([], [])
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(project.app, ["file-filter", "clear"])
context.result = result
context.mock_service = mock_service
@when('I remove project file filters include "{include_glob}" exclude "{exclude_glob}"')
def step_remove_file_filters(context, include_glob, exclude_glob):
"""Run the file-filter remove command with include and exclude globs."""
runner = CliRunner()
with patch("cleveragents.application.container.get_container") as mock_container:
mock_service = MagicMock()
mock_service.get_current_project.return_value = context.mock_project
mock_service.update_file_filters.return_value = context.mock_project
mock_service.get_project_filters.return_value = ([], [])
mock_container.return_value.project_service.return_value = mock_service
result = runner.invoke(
project.app,
[
"file-filter",
"remove",
"--include",
include_glob,
"--exclude",
exclude_glob,
],
)
context.result = result
context.mock_service = mock_service
context.remove_include = include_glob
context.remove_exclude = exclude_glob
@then("it should display the current file filters")
def step_check_file_filter_output(context):
"""Verify file-filter show output displays includes and excludes."""
assert context.result.exit_code == 0
assert "Project File Filters" in context.result.output
for pattern in context.project_includes + context.project_excludes:
assert pattern in context.result.output
@then("it should warn about the missing project")
def step_check_missing_project_warning(context):
"""Verify missing project warning and exit code."""
combined_output = context.result.output
try:
if context.result.stderr:
combined_output += context.result.stderr
except (ValueError, AttributeError):
pass
assert "No project found in current directory" in combined_output
@then("it should update filters with defaults applied")
def step_check_filters_updated_with_defaults(context):
"""Validate that defaults were included when adding filters."""
assert context.result.exit_code == 0
context.mock_service.update_file_filters.assert_called_once_with(
context.mock_project,
include_add=[context.expected_include],
exclude_add=context.expected_excludes,
)
assert "Updated Project File Filters" in context.result.output
for pattern in context.expected_excludes:
assert pattern in context.result.output
@then("it should clear both include and exclude filters")
def step_check_clear_filters(context):
"""Ensure clear command cleared both include and exclude filters."""
assert context.result.exit_code == 0
context.mock_service.update_file_filters.assert_called_once_with(
context.mock_project,
clear_include=True,
clear_exclude=True,
)
assert "Cleared Project File Filters" in context.result.output
@then("it should remove the specified file filters")
def step_check_remove_filters(context):
"""Ensure remove command removed provided filters."""
assert context.result.exit_code == 0
context.mock_service.update_file_filters.assert_called_once_with(
context.mock_project,
include_remove=[context.remove_include],
exclude_remove=[context.remove_exclude],
)
assert "Updated Project File Filters" in context.result.output
@then("the project command should succeed")
def step_project_command_should_succeed(context):
"""Assert the project command completed successfully."""
assert context.result.exit_code == 0
@then("the project command should exit with code {code:d}")
def step_project_command_exit_code(context, code):
"""Assert the project command exited with the expected code."""
assert context.result.exit_code == code, (
f"Expected exit code {code}, got {context.result.exit_code}. Output: {context.result.output}"
)
@then('the project list output should contain "{text}"')
def step_project_list_output_contains(context, text):
"""Assert the project list output contains expected text."""
assert context.result.exit_code == 0, (
f"Expected exit code 0, got {context.result.exit_code}. Output: {context.result.output}"
)
assert text in context.result.output, (
f"Expected '{text}' in output: {context.result.output}"
)