Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
x-fits llega vivo del contrato y se pone delante del modelo: es el único límite que nada rechaza, porque el renderizador encoge en vez de fallar. Sin esto el modelo escribe una cita de 58 caracteres para un hueco de 16 y se entera cuando ya está renderizada. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
186 lines
7.3 KiB
Python
186 lines
7.3 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, _session_from_filename, _video_predates_spec
|
|
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
|
|
|
|
|
|
# --- el parte de la subida a YouTube ----------------------------------------
|
|
|
|
def _uploaded(**kw):
|
|
from src.generator.youtube import UploadedVideo
|
|
base = dict(video_id="abc123", title="X", privacy_status="private")
|
|
base.update(kw)
|
|
return UploadedVideo(**base)
|
|
|
|
|
|
def test_upload_message_leads_with_the_studio_link():
|
|
"""El enlace de Studio es la acción; el de watch es sólo comprobación."""
|
|
from src.bot.bot import _upload_message
|
|
text = _upload_message(_uploaded(), {"snippet": {"tags": ["UAP"]}},
|
|
"https://theexclusionzone.com/x/")
|
|
assert "https://studio.youtube.com/video/abc123/edit" in text
|
|
assert "https://youtube.com/shorts/abc123" in text
|
|
|
|
|
|
def test_upload_message_explains_the_private_lock():
|
|
"""Que esté privado no es un fallo del bot, y hay que decir por qué."""
|
|
from src.bot.bot import _upload_message
|
|
text = _upload_message(_uploaded(), {}, "https://x.test/")
|
|
assert "PRIVADO" in text
|
|
assert "auditoría" in text
|
|
|
|
|
|
def test_upload_message_flags_a_forced_privacy_change():
|
|
from src.bot.bot import _upload_message
|
|
text = _upload_message(_uploaded(forced_private=True), {}, "https://x.test/")
|
|
assert "forzó" in text
|
|
|
|
|
|
def test_upload_message_warns_when_the_description_has_no_article():
|
|
from src.bot.bot import _upload_message
|
|
text = _upload_message(_uploaded(), {}, None)
|
|
assert "Sin URL de artículo" in text
|
|
assert "force" in text
|
|
|
|
|
|
def test_upload_message_is_plain_text():
|
|
"""Va sin parse_mode: lleva el título del modelo, y un Markdown roto haría
|
|
que Telegram rechazara justo el mensaje que trae el enlace."""
|
|
from src.bot.bot import _upload_message
|
|
text = _upload_message(_uploaded(title="JAL 1628: *three* radars_"), {}, None)
|
|
assert "*three*" in text and "radars_" in text
|
|
|
|
|
|
# --- guard de vídeo viejo en /upload_short ----------------------------------
|
|
|
|
class TestStaleVideoGuard:
|
|
"""`produce` guarda el spec ANTES de renderizar: si un re-intento falla,
|
|
en disco queda el vídeo de la vuelta anterior y subirlo le pondría los
|
|
metadatos del spec nuevo a un vídeo viejo."""
|
|
|
|
def test_a_fresh_render_is_never_stale(self):
|
|
# El MP4 se escribe ~1 min después de guardarse el spec.
|
|
assert not _video_predates_spec(1000.0 + 60, 1000.0)
|
|
|
|
def test_clock_jitter_does_not_cry_wolf(self):
|
|
assert not _video_predates_spec(1000.0 - 3, 1000.0)
|
|
|
|
def test_a_video_hours_older_than_the_spec_is_flagged(self):
|
|
assert _video_predates_spec(1000.0 - 3600, 1000.0)
|
|
|
|
|
|
class TestSessionFromFilename:
|
|
"""Telegram conserva el nombre del fichero al reenviarlo: el id que puso
|
|
/short_spec manda sobre la sesión activa del chat."""
|
|
|
|
def test_the_short_spec_filename_declares_its_session(self):
|
|
assert _session_from_filename("short_166_spec.json") == 166
|
|
|
|
def test_a_foreign_filename_falls_back_to_none(self):
|
|
assert _session_from_filename("myspec.json") is None
|
|
assert _session_from_filename("") is None
|
|
assert _session_from_filename(None) is None
|
|
|
|
|
|
class TestNarrationWarnings:
|
|
"""shortsmith manda por el mismo canal los textos recortados y los avisos
|
|
de la voz. Piden acciones distintas, así que se muestran distintos."""
|
|
|
|
def test_a_silent_shot_is_reported_as_such(self):
|
|
text = _claims_message(result_with(render_warnings=[
|
|
{"kind": "narration", "text": "shots.2.narration not spoken: piper exited 1"}]))
|
|
assert "🔇" in text and "shots.2.narration" in text
|
|
assert "recortados" not in text
|
|
|
|
def test_a_stretched_video_says_so(self):
|
|
text = _claims_message(result_with(render_warnings=[
|
|
{"kind": "timing",
|
|
"text": "narration stretched the video from 32.0s to 41.5s"}]))
|
|
assert "41.5s" in text
|
|
assert "recortados" not in text
|
|
|
|
def test_trimmed_text_and_narration_do_not_get_mixed_up(self):
|
|
text = _claims_message(result_with(render_warnings=[
|
|
{"template": "data_card", "text": "FILA LARGA", "requested": 44, "size": 38},
|
|
{"kind": "narration", "text": "no voice installed"}]))
|
|
assert "1 textos recortados" in text # sólo cuenta el de dibujo
|
|
assert "🔇 no voice installed" in text
|
|
|
|
|
|
class TestSevereShrink:
|
|
"""Un recorte leve es cosmético; uno grave deja el texto ilegible justo
|
|
donde importaba. Mezclarlos entrena al lector a ignorar los dos."""
|
|
|
|
def test_a_severe_shrink_is_called_out(self):
|
|
text = _claims_message(result_with(render_warnings=[
|
|
{"template": "document_quote", "text": "“UNA CITA MUY LARGA”",
|
|
"requested": 84, "size": 20, "severe": True}]))
|
|
assert "🔴" in text and "ILEGIBLES" in text
|
|
|
|
def test_a_cosmetic_shrink_stays_quiet(self):
|
|
text = _claims_message(result_with(render_warnings=[
|
|
{"template": "scale_bars", "text": "PHYSICAL EVIDENCE",
|
|
"requested": 92, "size": 84, "severe": False}]))
|
|
assert "recortados" in text
|
|
assert "ILEGIBLES" not in text and "🔴" not in text
|