"""Eval dorada: ¿podría este pipeline haber producido el vídeo que ya sabemos que está bien? Corre el generador entero contra una sesión REAL de investigación y compara el spec resultante con `examples/jal1628.json` — que es el que produjo el primer Short — de forma ESTRUCTURAL: número de shots, plantillas elegidas, duración total y claims sin fundamento. Nunca por igualdad de cadenas: el modelo redactará distinto y eso no es un fallo. Necesita una sesión de verdad, así que se salta salvo que se le dé todo: RESEARCHOWL_GOLDEN_DB=/ruta/a/researchowl.db \\ RESEARCHOWL_GOLDEN_SESSION=153 \\ SHORTSMITH_LIVE_URL=http://10.43.86.57:8080 \\ ANTHROPIC_API_KEY=... \\ pytest tests/test_short_golden.py -v -s Para sacar la sesión del cluster sin arrastrar la DB entera, `make golden-db`. """ import json import os from pathlib import Path import pytest EXAMPLE = Path(__file__).resolve().parents[1] / "src/generator/examples/jal1628.json" GOLDEN_DB = os.environ.get("RESEARCHOWL_GOLDEN_DB") GOLDEN_SESSION = os.environ.get("RESEARCHOWL_GOLDEN_SESSION") LIVE_URL = os.environ.get("SHORTSMITH_LIVE_URL") pytestmark = pytest.mark.skipif( not (GOLDEN_DB and GOLDEN_SESSION and LIVE_URL and os.environ.get("ANTHROPIC_API_KEY")), reason="la eval dorada necesita una sesión real, shortsmith vivo y clave de Claude") def structure(spec: dict) -> dict: """Lo comparable de un spec: forma, no palabras.""" shots = spec.get("shots", []) return { "shots": len(shots), "templates": [s["template"] for s in shots], "duration": sum(s["duration"] for s in shots), "distinct_templates": len({s["template"] for s in shots}), } @pytest.fixture(scope="module") def produced(): """Genera UNA vez (cuesta dinero) y reparte el resultado a los tests.""" import asyncio from src.config import settings settings.db_path = GOLDEN_DB settings.shortsmith_url = LIVE_URL settings.shortsmith_enabled = True from src.db.database import ResearchDB, close_db, get_db from src.generator.short import ShortProducer from src.processor.processor import ContentProcessor, OllamaClient async def run(): conn = await get_db() try: db = ResearchDB(conn) producer = ShortProducer(db, ContentProcessor(db, OllamaClient())) return await producer.produce(int(GOLDEN_SESSION), lambda text: print(" ", text)) finally: await close_db() return asyncio.run(run()) def test_the_pipeline_produces_a_renderable_short(produced): assert produced.spec is not None, produced.failure assert produced.failure is None, produced.failure assert produced.has_video assert Path(produced.video_path).stat().st_size > 100_000 def test_the_shape_matches_the_reference(produced): reference = structure(json.loads(EXAMPLE.read_text())) got = structure(produced.spec) print(f"\nreferencia: {reference}\nobtenido: {got}") # El vídeo bueno son 8 shots; ±3 sigue siendo la misma forma narrativa. assert abs(got["shots"] - reference["shots"]) <= 3 assert got["distinct_templates"] >= 4, "un Short de una sola plantilla es un cartel" assert 20.0 <= got["duration"] <= 45.0 # Un case_file abre con contexto y cierra con el contador: no se exige la # misma lista de plantillas, sí que el cierre sea un cierre. assert got["templates"][-1] == reference["templates"][-1] def test_no_claim_was_invented(produced): """Lo que de verdad decide si esto se puede publicar. Medido el 2026-08-01 contra la sesión 153, en dos tiradas: 36-37 claims de 38 casan. Lo que se escapa es de dos clases conocidas y ninguna se arregla endureciendo este assert: * "232 FT" (el largo de un 747) copiado del ejemplo de la sección 5, que no está en NINGUNO de los 126 chunks de la sesión — el comprobador lo etiqueta ya como fuga del ejemplo, no como invención; * una cita comprimida — "WALNUT SHAPED WIDE RIM" donde la fuente dice "walnut shaped with a wide rim around its circumference". El prompt ataca las dos, pero el muestreo del modelo varía entre tiradas, y un test que gasta $0.05 y depende del muestreo no sirve de puerta. **La puerta de verdad es el informe de claims en Telegram**, que se manda siempre. Esto sólo vigila que el fundamento no se desplome. """ report = produced.grounding print("\n" + report.summary()) assert len(report.grounded) >= 30, "el spec dejó de apoyarse en las fuentes" assert len(report.unsupported) <= 3, \ "sin fundamento: " + "; ".join(f"[{c.kind}] {c.text}" for c in report.unsupported) def test_it_did_not_take_many_attempts(produced): """Métrica del §4: si esto sube de 1.5 de media, lo que hay que arreglar es el prompt, no el número de reintentos.""" print(f"\nintentos hasta válido: {produced.attempts}; coste ${produced.cost_usd:.4f}") assert produced.attempts <= 2 def test_it_costs_what_a_blog_costs(produced): """Medido, no estimado: 40 chunks de contexto son ~25k tokens de entrada, y un reintento los paga otra vez. El §9 del spec calculaba $0.003-0.008 con un contexto mucho más corto; con este, dos intentos salen por ~$0.05. Sigue siendo lo que cuesta un /generate blog, que era el punto.""" assert produced.cost_usd < 0.08