diff --git a/src/bot/bot.py b/src/bot/bot.py index fccf726..4d3b9b7 100644 --- a/src/bot/bot.py +++ b/src/bot/bot.py @@ -21,7 +21,9 @@ from telegram.ext import ( from telegram.constants import ParseMode from src.config import settings -from src.db.database import get_db, close_db, ResearchDB, ResearchStatus, OutputType +from src.db.database import ( + get_db, close_db, ResearchDB, ResearchStatus, OutputType, RETENTION_DAYS, +) from src.scraper.exhaustive import ExhaustiveScraper from src.processor.processor import OllamaClient, ContentProcessor from src.generator.generator import OutputGenerator @@ -1346,7 +1348,7 @@ async def _purge_on_startup(app: Application) -> None: db_conn = await get_db() try: db = ResearchDB(db_conn) - result = await db.purge_old_data(30) + result = await db.purge_old_data(RETENTION_DAYS) # Cualquier borrado, no sólo el de sesiones. Con la retención por fecha # de output, una pasada puede llevarse 27 outputs y CERO sesiones — y # con la condición anterior eso no dejaba ni una línea de log. Una purga @@ -1590,7 +1592,7 @@ async def cmd_purge(update: Update, ctx: ContextTypes.DEFAULT_TYPE): args = ctx.args or [] if not args: - days = 30 + days = RETENTION_DAYS else: try: days = int(args[0]) diff --git a/src/db/database.py b/src/db/database.py index 550d7aa..be6ce0a 100644 --- a/src/db/database.py +++ b/src/db/database.py @@ -12,6 +12,18 @@ from src.config import settings logger = structlog.get_logger() +#: Cuánto se guarda. 90 días, no 30, y el motivo es que 30 no era una política +#: de retención: era una suposición sobre cuánto duraba el material. Los 17 +#: specs `short_en` del catálogo se escribieron entre el 2 y el 13 de agosto y +#: seguían siendo el material de trabajo el 1 de septiembre — tres semanas +#: después de que su ventana de 30 días empezara a correr. Con 90, lo que se +#: purga es lo que de verdad nadie va a volver a mirar. +#: +#: Escrito una sola vez a propósito: estaba en el arranque, en `/purge` y en la +#: firma por defecto, y tres copias de un plazo son tres plazos esperando a +#: divergir. +RETENTION_DAYS = 90 + class ResearchStatus(str, Enum): RUNNING = "running" @@ -648,7 +660,7 @@ class ResearchDB: # --- Maintenance --- - async def purge_old_data(self, max_age_days: int = 30) -> dict: + async def purge_old_data(self, max_age_days: int = RETENTION_DAYS) -> dict: """Retención en dos fases: primero los outputs por SU fecha, luego las sesiones que ya no sostienen nada. diff --git a/tests/test_short_producer.py b/tests/test_short_producer.py index 3f7afe4..456a1ba 100644 --- a/tests/test_short_producer.py +++ b/tests/test_short_producer.py @@ -339,7 +339,7 @@ async def test_purging_a_session_takes_its_video_with_it(tmp_path, monkeypatch): assert (shorts / "2.mp4").exists(), "la sesión reciente conserva su vídeo" -async def purge_fixture(tmp_path, monkeypatch, sessions, outputs, days=30): +async def purge_fixture(tmp_path, monkeypatch, sessions, outputs, days=None): """Una BD con sesiones y outputs de las edades que se le pidan, purgada. `sessions` y `outputs` llevan la edad en días: positiva es pasado. Devuelve @@ -380,7 +380,11 @@ async def purge_fixture(tmp_path, monkeypatch, sessions, outputs, days=30): await conn.commit() try: - counts = await ResearchDB(conn).purge_old_data(days) + db = ResearchDB(conn) + # `days=None` deja hablar al valor por defecto, que es lo que corre en + # producción — pasarlo a mano en cada test convierte la ventana real en + # algo que ninguna prueba mira. + counts = await (db.purge_old_data(days) if days else db.purge_old_data()) vivos = [r[0] for r in await (await conn.execute( "SELECT id FROM outputs ORDER BY id")).fetchall()] sesiones = [r[0] for r in await (await conn.execute( @@ -457,6 +461,28 @@ async def test_a_session_that_loses_its_last_short_loses_its_video(tmp_path, assert not (shorts / "1.mp4").exists() +@pytest.mark.asyncio +async def test_the_default_window_keeps_material_still_in_use(tmp_path, monkeypatch): + """La ventana por defecto, fijada por comportamiento y no por el número. + + 30 días no era una política de retención, era una suposición sobre cuánto + dura el material: los 17 specs del catálogo se escribieron entre el 2 y el + 13 de agosto y seguían siendo el material de trabajo el 1 de septiembre. + Este test falla si alguien vuelve a estrechar la ventana por debajo de mes + y medio, que es donde empieza a llevarse cosas que aún se usan. + """ + from src.db.database import RETENTION_DAYS + + counts, vivos, _, _ = await purge_fixture( + tmp_path, monkeypatch, + sessions=[(1, 120)], + outputs=[(1, 1, 45), (2, 1, 200)]) # uno de mes y medio, uno de siete meses + + assert RETENTION_DAYS >= 45 + assert vivos == [1], "un output de 45 días sigue siendo material de trabajo" + assert counts["outputs"] == 1, "y uno de 200 días no" + + @pytest.mark.asyncio async def test_the_youtube_url_never_passes_for_an_article_url(tmp_path): """Subir un Short escribe su URL de YouTube en `published_url`. Si