""" Pruebas de la retención por nodo (Procesados/) y por antigüedad (Fallados/). Se mockea el acceso a la BD (JobRepository/EventRepository) y se opera sobre carpetas reales en tmp_path. Las fechas-carpeta de Procesados/ se derivan con el mismo helper que usa el limpiador para que la prueba sea independiente de la zona horaria del runner. """ from datetime import datetime import pytest from app.engine.retention import RetentionCleaner class FakeJob: def __init__(self, job_id, node_name, source_name, finished_at): self.job_id = job_id self.node_name = node_name self.source_name = source_name self.finished_at = finished_at def _config(tmp_path, *, days=2, failed_days=7, dry_run=False): return { "paths": { "processed_folder": str(tmp_path / "processed"), "failed_folder": str(tmp_path / "failed"), }, "retention": {"days": days, "failed_days": failed_days, "dry_run": dry_run}, } def _date_folder(base, iso): """Crea (si falta) la carpeta-fecha local correspondiente a un finished_at ISO UTC.""" local_date = RetentionCleaner._local_date_from_iso(iso) folder = base / local_date.isoformat() folder.mkdir(parents=True, exist_ok=True) return folder def test_procesados_borra_obsoletos_conserva_reciente_y_nodo_unico(monkeypatch, tmp_path): processed = tmp_path / "processed" ref_iso = "2026-07-24T10:00:00" obsolete_iso = "2026-07-20T10:00:00" node_b_iso = "2026-07-22T10:00:00" # NODO_A: copia obsoleta (a borrar) + copia más reciente (a conservar). obsolete_file = _date_folder(processed, obsolete_iso) / "NODO_A.zip" obsolete_file.write_bytes(b"viejo") recent_file = _date_folder(processed, ref_iso) / "NODO_A.zip" recent_file.write_bytes(b"nuevo") # NODO_B: una sola restauración (nunca se toca). node_b_file = _date_folder(processed, node_b_iso) / "NODO_B.zip" node_b_file.write_bytes(b"unico") obsolete_job = FakeJob("job-a-old", "NODO_A", "NODO_A.zip", obsolete_iso) purged: list[str] = [] monkeypatch.setattr( "app.engine.retention.JobRepository.get_obsolete_completed_by_node", lambda days: [obsolete_job], ) monkeypatch.setattr( "app.engine.retention.JobRepository.get_latest_completed_per_node", lambda: {"NODO_A": ref_iso, "NODO_B": node_b_iso}, ) monkeypatch.setattr( "app.engine.retention.JobRepository.mark_purged", lambda job_id: purged.append(job_id) ) monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None) result = RetentionCleaner(_config(tmp_path)).run() assert not obsolete_file.exists() # obsoleto borrado assert recent_file.exists() # más reciente intacto assert node_b_file.exists() # nodo de una sola copia intacto assert purged == ["job-a-old"] assert result.deleted_files == 1 def test_procesados_dry_run_no_borra(monkeypatch, tmp_path): processed = tmp_path / "processed" obsolete_iso = "2026-07-20T10:00:00" obsolete_file = _date_folder(processed, obsolete_iso) / "NODO_A.zip" obsolete_file.write_bytes(b"viejo") monkeypatch.setattr( "app.engine.retention.JobRepository.get_obsolete_completed_by_node", lambda days: [FakeJob("job-a-old", "NODO_A", "NODO_A.zip", obsolete_iso)], ) monkeypatch.setattr( "app.engine.retention.JobRepository.get_latest_completed_per_node", lambda: {"NODO_A": "2026-07-24T10:00:00"}, ) purged: list[str] = [] monkeypatch.setattr( "app.engine.retention.JobRepository.mark_purged", lambda job_id: purged.append(job_id) ) monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None) result = RetentionCleaner(_config(tmp_path, dry_run=True)).run() assert obsolete_file.exists() # dry-run no borra assert purged == [] # ni marca purgado assert result.dry_run is True assert result.deleted_files == 1 # sí lo contabiliza como "se borraría" def test_fallados_borra_por_antiguedad_absoluta(monkeypatch, tmp_path): failed = tmp_path / "failed" old_folder = failed / "2026-07-10" # < (hoy - 7) recent_folder = failed / "2026-07-20" # >= (hoy - 7) old_folder.mkdir(parents=True) recent_folder.mkdir(parents=True) old_file = old_folder / "VIEJO.zip" old_file.write_bytes(b"x") recent_file = recent_folder / "RECIENTE.zip" recent_file.write_bytes(b"y") # Sin obsoletos en Procesados; solo probamos Fallados. monkeypatch.setattr( "app.engine.retention.JobRepository.get_obsolete_completed_by_node", lambda days: [] ) monkeypatch.setattr( "app.engine.retention.JobRepository.get_latest_completed_per_node", lambda: {} ) monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None) cleaner = RetentionCleaner( _config(tmp_path, failed_days=7), clock=lambda: datetime(2026, 7, 24, 12, 0, 0) ) cleaner.run() assert not old_file.exists() # carpeta-fecha vieja borrada assert recent_file.exists() # dentro de la ventana, se conserva def test_fallados_ignora_carpetas_no_fecha(monkeypatch, tmp_path): failed = tmp_path / "failed" weird = failed / "no-es-fecha" weird.mkdir(parents=True) keep = weird / "algo.zip" keep.write_bytes(b"z") monkeypatch.setattr( "app.engine.retention.JobRepository.get_obsolete_completed_by_node", lambda days: [] ) monkeypatch.setattr( "app.engine.retention.JobRepository.get_latest_completed_per_node", lambda: {} ) monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None) RetentionCleaner( _config(tmp_path), clock=lambda: datetime(2026, 7, 24, 12, 0, 0) ).run() assert keep.exists() # nombre que no es fecha: no se toca