290 lines
9.6 KiB
Python
290 lines
9.6 KiB
Python
"""Step definitions for CONC3 cleanup CLI commands and edge cases.
|
|
|
|
Exercises the Typer CLI commands (scan, purge, status) and
|
|
service-level edge cases for coverage.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.cleanup_service import (
|
|
CleanupReport,
|
|
CleanupService,
|
|
)
|
|
from cleveragents.config.settings import Settings
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_settings(**overrides: object) -> Settings:
|
|
"""Create a Settings instance with optional field overrides."""
|
|
Settings._instance = None
|
|
settings = Settings()
|
|
for key, value in overrides.items():
|
|
setattr(settings, key, value)
|
|
return settings
|
|
|
|
|
|
def _create_sandbox_dir(
|
|
prefix: str = "ca-sandbox-testplan-",
|
|
age_hours: float = 72,
|
|
) -> Path:
|
|
"""Create a fake sandbox temp directory with a given age."""
|
|
d = Path(tempfile.mkdtemp(prefix=prefix))
|
|
old_time = time.time() - (age_hours * 3600)
|
|
os.utime(d, (old_time, old_time))
|
|
return d
|
|
|
|
|
|
# ── CLI command tests ─────────────────────────────────────────────
|
|
|
|
|
|
@given("a cleanup CLI runner")
|
|
def step_cli_runner(context: Context) -> None:
|
|
from typer.testing import CliRunner
|
|
|
|
context.cli_runner = CliRunner()
|
|
|
|
|
|
@given("a stale sandbox for CLI testing")
|
|
def step_stale_sandbox_cli(context: Context) -> None:
|
|
import shutil
|
|
|
|
context.cli_sandbox_dir = _create_sandbox_dir(age_hours=72)
|
|
|
|
if not hasattr(context, "_temp_paths"):
|
|
context._temp_paths = []
|
|
context._temp_paths.append(context.cli_sandbox_dir)
|
|
|
|
def _cleanup() -> None:
|
|
for p in getattr(context, "_temp_paths", []):
|
|
if p.exists():
|
|
if p.is_dir():
|
|
shutil.rmtree(p, ignore_errors=True)
|
|
else:
|
|
p.unlink(missing_ok=True)
|
|
|
|
if _cleanup not in context._cleanup_handlers:
|
|
context._cleanup_handlers.append(_cleanup)
|
|
|
|
|
|
@when("I invoke the cleanup scan command")
|
|
def step_invoke_scan(context: Context) -> None:
|
|
from cleveragents.cli.commands.cleanup import app as cleanup_app
|
|
|
|
context.cli_result = context.cli_runner.invoke(cleanup_app, ["scan"])
|
|
|
|
|
|
@when("I invoke the cleanup purge command with dry-run")
|
|
def step_invoke_purge_dry(context: Context) -> None:
|
|
from cleveragents.cli.commands.cleanup import app as cleanup_app
|
|
|
|
context.cli_result = context.cli_runner.invoke(cleanup_app, ["purge", "--dry-run"])
|
|
|
|
|
|
@when("I invoke the cleanup purge command with yes flag")
|
|
def step_invoke_purge_yes(context: Context) -> None:
|
|
from cleveragents.cli.commands.cleanup import app as cleanup_app
|
|
|
|
context.cli_result = context.cli_runner.invoke(cleanup_app, ["purge", "--yes"])
|
|
|
|
|
|
@when("I invoke the cleanup purge command with all and yes")
|
|
def step_invoke_purge_all_yes(context: Context) -> None:
|
|
from cleveragents.cli.commands.cleanup import app as cleanup_app
|
|
|
|
context.cli_result = context.cli_runner.invoke(
|
|
cleanup_app, ["purge", "--all", "--yes"]
|
|
)
|
|
|
|
|
|
@when("I invoke the cleanup status command")
|
|
def step_invoke_status(context: Context) -> None:
|
|
from cleveragents.cli.commands.cleanup import app as cleanup_app
|
|
|
|
context.cli_result = context.cli_runner.invoke(cleanup_app, ["status"])
|
|
|
|
|
|
@when("I invoke the cleanup purge command without confirmation")
|
|
def step_invoke_purge_no_confirm(context: Context) -> None:
|
|
from cleveragents.cli.commands.cleanup import app as cleanup_app
|
|
|
|
context.cli_result = context.cli_runner.invoke(cleanup_app, ["purge"], input="n\n")
|
|
|
|
|
|
@then("the CLI exit code should be {code:d}")
|
|
def step_cli_exit_code(context: Context, code: int) -> None:
|
|
assert context.cli_result.exit_code == code, (
|
|
f"Expected exit code {code}, got {context.cli_result.exit_code}. "
|
|
f"Output: {context.cli_result.output}"
|
|
)
|
|
|
|
|
|
@then('the cleanup CLI output should contain "{text}"')
|
|
def step_cleanup_cli_output_contains(context: Context, text: str) -> None:
|
|
assert text in context.cli_result.output, (
|
|
f"Expected '{text}' in output, got: {context.cli_result.output}"
|
|
)
|
|
|
|
|
|
# ── CleanupReport and CleanupService edge case tests ─────────────
|
|
|
|
|
|
@when("I create a cleanup report with known values")
|
|
def step_create_report(context: Context) -> None:
|
|
report = CleanupReport(dry_run=True)
|
|
report.sandboxes.scanned = 5
|
|
report.sandboxes.removed = 2
|
|
report.checkpoints.scanned = 100
|
|
context.report_dict = report.as_dict()
|
|
|
|
|
|
@then('the report dict should contain key "{key}"')
|
|
def step_report_dict_key(context: Context, key: str) -> None:
|
|
assert key in context.report_dict, (
|
|
f"Expected key '{key}' in report dict, got: {list(context.report_dict.keys())}"
|
|
)
|
|
|
|
|
|
@when("I create a cleanup service with invalid settings")
|
|
def step_invalid_settings(context: Context) -> None:
|
|
try:
|
|
CleanupService(settings="not-a-settings") # type: ignore[arg-type]
|
|
context.type_error_raised = False
|
|
except TypeError:
|
|
context.type_error_raised = True
|
|
|
|
|
|
@then("a TypeError should be raised")
|
|
def step_type_error(context: Context) -> None:
|
|
assert context.type_error_raised, "Expected TypeError to be raised"
|
|
|
|
|
|
@given("a cleanup service for age description testing")
|
|
def step_age_service(context: Context) -> None:
|
|
context.settings = _make_settings()
|
|
context.service = CleanupService(
|
|
settings=context.settings,
|
|
active_plan_ids=frozenset(),
|
|
)
|
|
|
|
|
|
@when("I check the age description for a {hours:d} hour old file")
|
|
def step_age_hours(context: Context, hours: int) -> None:
|
|
tmp = Path(tempfile.mktemp(prefix="ca-age-test-"))
|
|
tmp.write_text("test")
|
|
old_time = time.time() - (hours * 3600)
|
|
os.utime(tmp, (old_time, old_time))
|
|
context.age_desc = CleanupService._age_description(tmp)
|
|
tmp.unlink()
|
|
|
|
|
|
@when("I check the age description for a {minutes:d} minute old file")
|
|
def step_age_minutes(context: Context, minutes: int) -> None:
|
|
tmp = Path(tempfile.mktemp(prefix="ca-age-test-"))
|
|
tmp.write_text("test")
|
|
old_time = time.time() - (minutes * 60)
|
|
os.utime(tmp, (old_time, old_time))
|
|
context.age_desc = CleanupService._age_description(tmp)
|
|
tmp.unlink()
|
|
|
|
|
|
@when("I check the age description for a {days:d} day old file")
|
|
def step_age_days(context: Context, days: int) -> None:
|
|
tmp = Path(tempfile.mktemp(prefix="ca-age-test-"))
|
|
tmp.write_text("test")
|
|
old_time = time.time() - (days * 86400)
|
|
os.utime(tmp, (old_time, old_time))
|
|
context.age_desc = CleanupService._age_description(tmp)
|
|
tmp.unlink()
|
|
|
|
|
|
@then('the age description should contain "{text}"')
|
|
def step_age_contains(context: Context, text: str) -> None:
|
|
assert text in context.age_desc, (
|
|
f"Expected '{text}' in age description, got: {context.age_desc}"
|
|
)
|
|
|
|
|
|
# ── Full scan/purge integration via service ──────────────────────
|
|
|
|
|
|
@when("I run a full scan")
|
|
def step_full_scan(context: Context) -> None:
|
|
context.report = context.service.scan()
|
|
|
|
|
|
@then("the report should contain log stale items")
|
|
def step_log_stale_items(context: Context) -> None:
|
|
log_items = [i for i in context.report.stale_items if i.resource_type == "log"]
|
|
assert len(log_items) >= 1, f"Expected >= 1 log stale item, found {len(log_items)}"
|
|
|
|
|
|
@then("the report should contain backup stale items")
|
|
def step_backup_stale_items(context: Context) -> None:
|
|
backup_items = [
|
|
i for i in context.report.stale_items if i.resource_type == "backup"
|
|
]
|
|
assert len(backup_items) >= 1, (
|
|
f"Expected >= 1 backup stale item, found {len(backup_items)}"
|
|
)
|
|
|
|
|
|
@when("I run purge all")
|
|
def step_run_purge_all(context: Context) -> None:
|
|
context.report = context.service.purge(purge_all=True)
|
|
|
|
|
|
@then("the backup file should be removed")
|
|
def step_backup_removed(context: Context) -> None:
|
|
assert not context.backup_file.exists(), (
|
|
f"Backup file {context.backup_file} still exists"
|
|
)
|
|
|
|
|
|
@when("I run a full checkpoint scan")
|
|
def step_full_checkpoint_scan(context: Context) -> None:
|
|
context.service._get_checkpoint_base_dir = lambda: context.checkpoint_dir.parent
|
|
plan_dir = context.checkpoint_dir.parent / "test-plan"
|
|
if not plan_dir.exists():
|
|
context.checkpoint_dir.rename(plan_dir)
|
|
context.checkpoint_dir = plan_dir
|
|
context.service._get_checkpoint_base_dir = lambda: plan_dir.parent
|
|
context.report = context.service.scan()
|
|
|
|
|
|
@then("the report should contain checkpoint stale items")
|
|
def step_checkpoint_stale_items(context: Context) -> None:
|
|
cp_items = [
|
|
i for i in context.report.stale_items if i.resource_type == "checkpoint"
|
|
]
|
|
assert len(cp_items) >= 1, (
|
|
f"Expected >= 1 checkpoint stale item, found {len(cp_items)}"
|
|
)
|
|
|
|
|
|
@when("I run purge for checkpoints")
|
|
def step_purge_checkpoints_full(context: Context) -> None:
|
|
plan_dir = context.checkpoint_dir
|
|
context.service._get_checkpoint_base_dir = lambda: plan_dir.parent
|
|
target = plan_dir.parent / "test-plan-purge"
|
|
if not target.exists():
|
|
plan_dir.rename(target)
|
|
context.checkpoint_dir = target
|
|
context.service._get_checkpoint_base_dir = lambda: target.parent
|
|
context.report = context.service.purge()
|
|
|
|
|
|
@then("the report should show checkpoints removed")
|
|
def step_checkpoints_removed_report(context: Context) -> None:
|
|
assert context.report.checkpoints.removed >= 1, (
|
|
f"Expected >= 1 checkpoint removed, got {context.report.checkpoints.removed}"
|
|
)
|