"""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}" )