Files
researchowl/tests/test_shortsmith_live.py
T
ChemaVXandClaude Opus 5 198b0e6238
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m18s
fix(short): los textos ilegibles se avisaban con el render ya pagado
La nota que teníamos era falsa: los avisos `severe` de shortsmith SÍ llegan a
un humano — el informe de Telegram los saca en rojo. Lo que pasa es que llegan
colgados del render TERMINADO, así que hacerles caso significa editar el spec a
mano y pagar un segundo. Por eso nadie actuó nunca.

`x-fits` tampoco podía ser la comprobación, y a propósito: es guía blanda que el
propio ejemplo de referencia se salta por uno o tres caracteres viéndose bien.
Parar ahí sería gritar con specs buenos, y un aviso que grita se ignora — que es
justo cómo sobrevivieron los graves. Así que shortsmith publica desde 289d50e un
segundo número medido por campo, `x-fits-hard`, y esto lo comprueba antes de
gastar el render: cuesta un reintento del modelo en vez de un render. El mismo
movimiento que hizo `MAX_CUE_CHARS` con los captions en e32c59f.

Auditado contra los 17 short_en de producción: OCHO llevan al menos un texto que
se dibuja ilegible.

- El ya conocido, Cash-Landrum: `ALL THREE DEVELOPED SYMPTOMS CONSISTENT WITH
  RADIATION EXPOSURE`, 36 px pedidos y 20 dibujados. Salta con 63 caracteres
  contra un presupuesto de 61 — así de vertical es la curva pegada al muro.
- El que nadie había visto es peor y más común: el CARTEL DE CIERRE.
  `counter_close.lines` pide 110 px y se dibujó a 28 en el peor caso, y por
  debajo de 64 en cinco de los ocho. Es la llamada a la acción, y en un tercio
  del catálogo es el texto más pequeño del fotograma.
- Y la tolerancia aguanta sobre datos reales: `STILL UNEXPLAINED` encoge de 110
  a 84 px y no se avisa, que es lo correcto.

El desempate entre intentos pasa a contar averías —gancho y texto ilegible—
antes que segundos, por la misma razón que en 6d9b602: una reescritura que
arregla un rótulo pero se pasa un segundo perdía contra la que no lo arreglaba.

Un campo sin presupuesto propio (`Bar.unit`, que se dibuja dentro de la cadena
de `value_label`) se salta a conciencia: reconstruir esa cadena aquí pediría
conocer el formato de la plantilla, que es lo que este repo no sabe ni debe.

Y contra un shortsmith anterior a 289d50e esto se calla — no puede inventarse el
número —, así que de que el contrato traiga el campo se encarga
test_shortsmith_live.py, que es quien habla con el servicio.

Suite: 274 pasan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 15:31:05 +00:00

82 lines
3.0 KiB
Python

"""Prueba de fontanería contra un shortsmith VIVO.
Se salta salvo que se le dé una URL alcanzable desde donde corren los tests:
SHORTSMITH_LIVE_URL=http://10.43.86.57:8080 pytest tests/test_shortsmith_live.py -v
(dentro del cluster es `http://shortsmith-svc.shortsmith.svc.cluster.local:8080`;
desde el nodo, la ClusterIP de `kubectl get svc -n shortsmith`).
Renderiza el ejemplo de referencia entero — ~30 s de CPU en el pod — y comprueba
que vuelve un MP4. Es el paso 1 del §12 del spec de fase 2: probar el transporte
antes de generar nada.
"""
import json
import os
from pathlib import Path
import pytest
from src.generator.shortsmith import ShortsmithClient
LIVE_URL = os.environ.get("SHORTSMITH_LIVE_URL")
pytestmark = pytest.mark.skipif(
not LIVE_URL, reason="define SHORTSMITH_LIVE_URL para probar contra el servicio vivo")
EXAMPLE = Path(__file__).resolve().parents[1] / "src/generator/examples/jal1628.json"
@pytest.mark.asyncio
async def test_healthz_and_templates():
client = ShortsmithClient(LIVE_URL)
health = await client.health()
assert health["status"] == "ok"
assert health["templates"] >= 1
templates = await client.templates(refresh=True)
# No se comprueban nombres concretos a propósito: el contrato es de
# shortsmith y añadir plantillas allí no debe romper aquí.
assert templates, "GET /templates devolvió vacío"
for name, schema in templates.items():
assert schema.get("type") == "object", f"{name} no publica un esquema de objeto"
assert "properties" in schema
# Los dos presupuestos de texto, en el contrato SERVIDO. `unreadable_notes`
# se calla contra un esquema que no los traiga —no puede inventarse el
# número—, así que un shortsmith anterior a 289d50e apagaría la comprobación
# entera sin un solo error. Esto es lo único que lo nota.
sin_tope = [
f"{name}.{prop}"
for name, schema in templates.items()
for owner in [schema, *(schema.get("$defs") or {}).values()]
for prop, node in (owner.get("properties") or {}).items()
if "x-fits" in node and "x-fits-hard" not in node
]
assert sin_tope == [], f"campos con x-fits pero sin x-fits-hard: {sin_tope}"
@pytest.mark.asyncio
async def test_render_the_reference_example_end_to_end(tmp_path):
client = ShortsmithClient(LIVE_URL)
spec = json.loads(EXAMPLE.read_text())
job_id = await client.render(spec)
seen = []
async def on_progress(fraction, status):
seen.append(fraction)
result = await client.poll(job_id, on_progress=on_progress)
assert result.ok, f"el job terminó en {result.status}: {result.error}"
assert seen and max(seen) == 1.0
video = await client.fetch_video(job_id)
# ftyp en los primeros bytes: es un MP4 de verdad, no una página de error.
assert b"ftyp" in video[:32]
assert len(video) > 100_000, f"solo {len(video)} bytes — sospechosamente corto"
out = tmp_path / "jal1628.mp4"
out.write_bytes(video)
print(f"\nrenderizado {len(video)/1e6:.2f} MB en {out}")