Files
temp/features/steps/cleanup_cli_uncovered_branches_steps.py

228 lines
7.7 KiB
Python

"""Step definitions for cleanup CLI uncovered branches.
Covers missed lines and branches in
``cleveragents.cli.commands.cleanup``:
* L49-50 - inner ``OSError`` on ``p.stat()`` inside ``_detect_active_plan_ids``
* L52-58 - outer ``OSError`` from ``_get_sandbox_dirs`` -> warning + empty frozenset
* L44->42 - empty sandbox dir list (loop end)
* L87->94 - ``scan()`` with no stale items -> "No stale resources found."
* L128-131 - ``purge --dry-run`` with stale items -> "Would clean:" listing
* L139->143 - ``purge`` confirmation prompt accepted -> proceeds
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.cleanup_service import (
CleanupReport,
StaleItem,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _empty_report() -> CleanupReport:
"""Return a ``CleanupReport`` with no stale items (dry-run)."""
return CleanupReport(dry_run=True)
def _report_with_stale_items() -> CleanupReport:
"""Return a dry-run ``CleanupReport`` containing stale items."""
report = CleanupReport(dry_run=True)
report.sandboxes.scanned = 1
report.sandboxes.removed = 1
report.stale_items = [
StaleItem(
resource_type="sandbox",
path="/tmp/fake-sandbox",
age_description="3 days old",
plan_id="plan-001",
),
]
return report
def _purge_report() -> CleanupReport:
"""Return a non-dry-run ``CleanupReport`` for purge results."""
report = CleanupReport(dry_run=False)
report.sandboxes.scanned = 1
report.sandboxes.removed = 1
return report
# ---------------------------------------------------------------------------
# Givens
# ---------------------------------------------------------------------------
@given("cleanup cli branch mock service returns empty report")
def step_mock_service_empty_report(context: Context) -> None:
mock_svc = MagicMock()
mock_svc.scan.return_value = _empty_report()
context._cleanup_branch_mock_svc = mock_svc
@given("cleanup cli branch mock service returns report with stale items")
def step_mock_service_stale_report(context: Context) -> None:
mock_svc = MagicMock()
mock_svc.scan.return_value = _report_with_stale_items()
context._cleanup_branch_mock_svc = mock_svc
@given("cleanup cli branch mock service returns purge report")
def step_mock_service_purge_report(context: Context) -> None:
mock_svc = MagicMock()
mock_svc.scan.return_value = _empty_report()
mock_svc.purge.return_value = _purge_report()
context._cleanup_branch_mock_svc = mock_svc
@given("cleanup cli branch a service whose sandbox dir stat raises OSError")
def step_service_stat_oserror(context: Context) -> None:
"""Build a mock service where one sandbox path raises OSError on stat().
``extract_plan_id_from_sandbox`` returns a truthy plan ID so the code
enters the ``try: mtime = p.stat()`` block and hits the inner ``except
OSError: pass`` (L49-50).
"""
bad_path = MagicMock(spec=Path)
bad_path.stat.side_effect = OSError("permission denied")
mock_svc = MagicMock()
mock_svc._get_sandbox_dirs.return_value = [bad_path]
mock_svc.extract_plan_id_from_sandbox.return_value = "plan-abc"
context._cleanup_branch_mock_svc = mock_svc
@given("cleanup cli branch a service whose get sandbox dirs raises OSError")
def step_service_get_sandbox_dirs_oserror(context: Context) -> None:
mock_svc = MagicMock()
mock_svc._get_sandbox_dirs.side_effect = OSError("cannot list /tmp")
context._cleanup_branch_mock_svc = mock_svc
@given("cleanup cli branch a service with no sandbox dirs")
def step_service_empty_sandbox_dirs(context: Context) -> None:
mock_svc = MagicMock()
mock_svc._get_sandbox_dirs.return_value = []
context._cleanup_branch_mock_svc = mock_svc
# ---------------------------------------------------------------------------
# Whens
# ---------------------------------------------------------------------------
@when("cleanup cli branch I invoke scan via main app")
def step_invoke_scan(context: Context) -> None:
from typer.testing import CliRunner
from cleveragents.cli.main import app
runner = CliRunner()
with patch(
"cleveragents.cli.commands.cleanup._get_cleanup_service",
return_value=context._cleanup_branch_mock_svc,
):
context._cleanup_branch_result = runner.invoke(app, ["cleanup", "scan"])
@when("cleanup cli branch I invoke purge dry-run via main app")
def step_invoke_purge_dry_run(context: Context) -> None:
from typer.testing import CliRunner
from cleveragents.cli.main import app
runner = CliRunner()
with patch(
"cleveragents.cli.commands.cleanup._get_cleanup_service",
return_value=context._cleanup_branch_mock_svc,
):
context._cleanup_branch_result = runner.invoke(
app, ["cleanup", "purge", "--dry-run"]
)
@when("cleanup cli branch I invoke purge with confirmation y via main app")
def step_invoke_purge_confirm_y(context: Context) -> None:
from typer.testing import CliRunner
from cleveragents.cli.main import app
runner = CliRunner()
with patch(
"cleveragents.cli.commands.cleanup._get_cleanup_service",
return_value=context._cleanup_branch_mock_svc,
):
context._cleanup_branch_result = runner.invoke(
app, ["cleanup", "purge"], input="y\n"
)
@when("cleanup cli branch I call detect active plan ids")
def step_call_detect_active_plan_ids(context: Context) -> None:
from cleveragents.cli.commands.cleanup import _detect_active_plan_ids
# Capture stderr output for the warning assertion
context._cleanup_branch_warnings: list[str] = []
mock_err_console = MagicMock()
def capture_print(*args, **kwargs):
context._cleanup_branch_warnings.append(str(args))
mock_err_console.print = capture_print
with patch(
"cleveragents.cli.commands.cleanup.get_err_console",
return_value=mock_err_console,
):
context._cleanup_branch_detect_result = _detect_active_plan_ids(
context._cleanup_branch_mock_svc,
)
# ---------------------------------------------------------------------------
# Thens
# ---------------------------------------------------------------------------
@then("cleanup cli branch exit code is {code:d}")
def step_check_exit_code(context: Context, code: int) -> None:
result = context._cleanup_branch_result
assert result.exit_code == code, (
f"Expected exit code {code}, got {result.exit_code}.\n"
f"Output: {result.output}\n"
f"Exception: {result.exception!r}"
)
@then('cleanup cli branch output contains "{text}"')
def step_output_contains(context: Context, text: str) -> None:
output = context._cleanup_branch_result.output
assert text in output, f"Expected '{text}' in output, got:\n{output}"
@then("cleanup cli branch the result is an empty frozenset")
def step_result_empty_frozenset(context: Context) -> None:
result = context._cleanup_branch_detect_result
assert isinstance(result, frozenset), f"Expected frozenset, got {type(result)}"
assert len(result) == 0, f"Expected empty frozenset, got {result}"
@then("cleanup cli branch a warning was printed about active plans")
def step_warning_printed(context: Context) -> None:
warnings = context._cleanup_branch_warnings
combined = " ".join(warnings)
assert "active plans" in combined.lower() or "Warning" in combined, (
f"Expected warning about active plans, got: {combined}"
)