Files
researchowl/tests/test_bot_short_report.py
T
ChemaVXandClaude Opus 5 20c8d03aa7
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
feat(short): generación y render de Shorts vía shortsmith
Añade /generate short_en y /short_spec. El pipeline genera un shot spec
con Haiku, verifica cada cifra, fecha y cita contra los chunks de la
sesión, lo renderiza en shortsmith y entrega el MP4 por Telegram junto
a un informe de claims.

- ShortsmithClient con sondeo y fallback al spec JSON si el render falla
- Contrato de plantillas obtenido de GET /templates, no codificado
- Comprobación de fundamento determinista, sin LLM
- outputs.published_url para enlazar el artículo de Ghost
- Normalización de comillas rectas a tipográficas (ver KNOWN-ISSUES.md)

Lo que no aparece en los chunks se contrasta contra el ejemplo del
prompt: si casa ahí es fuga, no invención, y se informa como tal. El
purgado de sesiones se lleva también su MP4.

La subida a YouTube queda fuera a propósito: fase 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:55:42 +00:00

65 lines
2.2 KiB
Python

"""El mensaje de revisión del Short.
Es la puerta humana: si este mensaje no sale, o sale sin los avisos, se está
publicando lo que el modelo recuerde en vez de lo que dicen las fuentes. Por eso
tiene test propio aparte del pipeline.
"""
import json
from src.bot.bot import _claims_message
from src.generator.grounding import check_grounding
from src.generator.short import ShortResult
SPEC = {
"version": 1,
"meta": {"id": "x", "title": "X"},
"shots": [{"template": "scale_bars", "duration": 30.0, "props": {
"headline": "REPORTED SCALE",
"bars": [{"label": "BOEING 747", "value": 232, "unit": "FT"}]}}],
}
CHUNKS = [{"content": "A Boeing 747 is 232 ft long.", "url": "https://a.test/1"}]
def result_with(**kw):
base = dict(topic="Caso X", spec=SPEC, title="X", attempts=1,
cost_usd=0.0042, duration_s=30.0,
article_url="https://www.theexclusionzone.com/caso-x/",
grounding=check_grounding(SPEC, CHUNKS))
base.update(kw)
return ShortResult(**base)
def test_a_clean_report_still_says_so():
"""Un éxito silencioso enseña al lector a dejar de mirar."""
text = _claims_message(result_with())
assert "0 sin encontrar" in text
assert "1 chunks de 1 URLs" in text
assert "Coste: $0.0042" in text
def test_ungrounded_claims_are_listed_one_by_one():
invented = json.loads(json.dumps(SPEC))
invented["shots"][0]["props"]["headline"] = "41,000 FT"
text = _claims_message(result_with(spec=invented,
grounding=check_grounding(invented, CHUNKS)))
assert "1 sin encontrar" in text
assert "41,000 FT" in text
def test_a_session_without_an_article_url_says_what_to_run():
text = _claims_message(result_with(article_url=None))
assert "/generate blog en" in text
def test_render_warnings_reach_the_human():
text = _claims_message(result_with(render_warnings=[
{"template": "data_card", "text": "UNA FILA DEMASIADO LARGA",
"requested": 44, "size": 38}]))
assert "recortados" in text and "data_card" in text
def test_there_is_a_report_even_when_there_was_no_spec():
text = _claims_message(ShortResult(topic="Caso X"))
assert "Sin comprobación de fundamento" in text
assert "Coste:" in text