from __future__ import annotations import importlib.util from dataclasses import dataclass from datetime import datetime, timedelta from pathlib import Path from types import ModuleType def load_cleanup_module() -> ModuleType: script = Path(__file__).resolve().parents[2] / "scripts" / "cleanup_demo_artifacts.py" spec = importlib.util.spec_from_file_location("cleanup_demo_artifacts", script) assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module @dataclass class ExportRow: id: str created_at: datetime | None storage_path: str def test_cleanup_candidate_selection_keeps_newest_exports() -> None: cleanup = load_cleanup_module() base = datetime(2026, 1, 1, 12, 0, 0) exports = [ ExportRow("old", base, "/tmp/old.json"), ExportRow("new", base + timedelta(days=2), "/tmp/new.json"), ExportRow("middle", base + timedelta(days=1), "/tmp/middle.json"), ExportRow("unknown", None, "/tmp/unknown.json"), ] kept, candidates = cleanup.select_cleanup_candidates(exports, keep_latest=2) assert [export.id for export in kept] == ["new", "middle"] assert [export.id for export in candidates] == ["old", "unknown"] def test_cleanup_candidate_selection_rejects_negative_keep_latest() -> None: cleanup = load_cleanup_module() try: cleanup.select_cleanup_candidates([], keep_latest=-1) except ValueError as exc: assert "keep_latest" in str(exc) else: raise AssertionError("negative keep_latest should fail") def test_cleanup_path_safety_requires_storage_root_containment(tmp_path: Path) -> None: cleanup = load_cleanup_module() storage_root = tmp_path / "storage" safe_export = storage_root / "exports" / "project" / "report.html" unsafe_export = tmp_path / "outside" / "report.html" safe_export.parent.mkdir(parents=True) unsafe_export.parent.mkdir(parents=True) assert cleanup.is_within_storage_root(safe_export, storage_root) is True assert cleanup.is_within_storage_root(unsafe_export, storage_root) is False def test_cleanup_script_defaults_to_explicit_demo_project() -> None: cleanup = load_cleanup_module() parser = cleanup.build_parser() args = parser.parse_args([]) assert args.project_name == cleanup.DEMO_PROJECT_NAME assert args.keep_latest == 3 assert args.apply is False def test_cleanup_script_reports_dry_run_candidates_separately() -> None: script = Path(__file__).resolve().parents[2] / "backend" / "scripts" / "cleanup_demo_artifacts.py" content = script.read_text(encoding="utf-8") assert '"candidate_files": []' in content assert 'summary["candidate_files"].append(str(path))' in content