fix(db): 90 días, porque 30 no era una política sino una suposición
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
30 días no medía cuánto hay que guardar: medía cuánto se creía que 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 empezara a correr—, y el 31 de agosto se re-renderizaron tres de ellos. Con 90 días, lo que se purga es lo que de verdad nadie va a volver a mirar. El plazo se escribe UNA vez. Estaba en el arranque, en `/purge` y en la firma por defecto: tres copias de un plazo son tres plazos esperando a divergir, y ese es el mismo error de clase que tenía el tope de palabras por línea antes de derivarse de la duración del plano. El test nuevo fija la ventana por COMPORTAMIENTO y no por el número: un output de 45 días sobrevive y uno de 200 no. Estrecharla por debajo de mes y medio lo tumba, que es donde empieza a llevarse cosas que aún se usan. Y el fixture de purga pasa a llamar al valor por defecto cuando no se le dice otro, porque pasarle el plazo a mano en cada test dejaba el de producción sin que ninguna prueba lo mirara. Suite: 279 pasan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+5
-3
@@ -21,7 +21,9 @@ from telegram.ext import (
|
|||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
|
|
||||||
from src.config import settings
|
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.scraper.exhaustive import ExhaustiveScraper
|
||||||
from src.processor.processor import OllamaClient, ContentProcessor
|
from src.processor.processor import OllamaClient, ContentProcessor
|
||||||
from src.generator.generator import OutputGenerator
|
from src.generator.generator import OutputGenerator
|
||||||
@@ -1346,7 +1348,7 @@ async def _purge_on_startup(app: Application) -> None:
|
|||||||
db_conn = await get_db()
|
db_conn = await get_db()
|
||||||
try:
|
try:
|
||||||
db = ResearchDB(db_conn)
|
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
|
# 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
|
# 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
|
# 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 []
|
args = ctx.args or []
|
||||||
|
|
||||||
if not args:
|
if not args:
|
||||||
days = 30
|
days = RETENTION_DAYS
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
days = int(args[0])
|
days = int(args[0])
|
||||||
|
|||||||
+13
-1
@@ -12,6 +12,18 @@ from src.config import settings
|
|||||||
|
|
||||||
logger = structlog.get_logger()
|
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):
|
class ResearchStatus(str, Enum):
|
||||||
RUNNING = "running"
|
RUNNING = "running"
|
||||||
@@ -648,7 +660,7 @@ class ResearchDB:
|
|||||||
|
|
||||||
# --- Maintenance ---
|
# --- 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
|
"""Retención en dos fases: primero los outputs por SU fecha, luego las
|
||||||
sesiones que ya no sostienen nada.
|
sesiones que ya no sostienen nada.
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
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.
|
"""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
|
`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()
|
await conn.commit()
|
||||||
|
|
||||||
try:
|
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(
|
vivos = [r[0] for r in await (await conn.execute(
|
||||||
"SELECT id FROM outputs ORDER BY id")).fetchall()]
|
"SELECT id FROM outputs ORDER BY id")).fetchall()]
|
||||||
sesiones = [r[0] for r in await (await conn.execute(
|
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()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_the_youtube_url_never_passes_for_an_article_url(tmp_path):
|
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
|
"""Subir un Short escribe su URL de YouTube en `published_url`. Si
|
||||||
|
|||||||
Reference in New Issue
Block a user