La infografía de Socorro salió con «Lt. Colonel Hector Quintanilla» cuando el artículo dice «Major» las cinco veces que lo nombra. El rango NO lo inventó el modelo que maqueta: venía en el guion, y el verificador lo dejó pasar por dos motivos independientes. 1. RE_ARRANQUE veía el punto de «Lt.» y daba por hecho que «Colonel» iba en mayúscula por empezar frase, así que la eximía de comprobar. La única palabra capaz de cazar el error era justo la que se saltaba. Ahora una lista de abreviaturas conocidas anula esa exención. 2. _en_articulo comparaba por SUBCADENA: «Lt» daba positivo por estar dentro de built, adults y consultant. Pasa a exigir frontera de palabra por la izquierda — se conserva la laxitud por la derecha para que «Zamora's» del artículo siga casando con «Zamora» del guion. Y se añade la comprobación que faltaba, cargos_fuera(): los cargos y rangos se verifican en FRASE ENTERA. El fallo real no fue una palabra inventada sino una COMBINACIÓN falsa de dos palabras que sí estaban por separado, y eso un verificador token a token no puede verlo nunca. Medido antes de dejarlo puesto, sobre los DOS guiones reales de hoy (156 líneas): caza la avería, deja pasar la versión corregida, y cero falsos positivos de cargo. Por el camino de producción, filtra() tira solo las dos frases que llevan el rango malo y no toca el resto. Once tests. Incluye los dos lados: que pode lo que debe y que NO pode lo que no debe, porque un verificador que se pasa deja el guion cojo y obliga a rehacerlo a mano. También: hreflang-parejas.md con los slugs nuevos de los dos remates de hoy (socorro-1964-zamora-ufo-landing y cash-landrum-1980-demanda-ovni-tribunal-federal), corregidos por el propio agente del remate al leer la nota que lo pedía. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
6.4 KiB
Python
145 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests de infografia_brief.py — que el guion no contradiga al artículo.
|
|
|
|
Todo lo de aquí es la cicatriz de un fallo REAL del 2026-07-30: la infografía
|
|
de Socorro salió publicando «Lt. Colonel Hector Quintanilla» cuando el artículo
|
|
dice «Major» las cinco veces que lo nombra. El rango no lo inventó el modelo que
|
|
maqueta la imagen: venía en el guion, y el verificador lo dejó pasar por DOS
|
|
motivos independientes, así que hay dos redes que probar.
|
|
|
|
python3 -m pytest test_infografia_brief.py -q
|
|
python3 test_infografia_brief.py # sin pytest
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import infografia_brief as I
|
|
|
|
# Artículo de mentira con la forma del real: nombra a Quintanilla como Major, y
|
|
# contiene «consultant» y «built», que es donde se escondía «Lt» por subcadena.
|
|
ART = I.pliega(
|
|
"The Socorro UFO Landing of 1964 left four depressions and no explanation. "
|
|
"Project Blue Book chief, Major Hector Quintanilla, dispatched his "
|
|
"scientific consultant, Dr. J. Allen Hynek, to the site. Sergeant Sam "
|
|
"Chavez reached the arroyo first. The shack had been built years earlier. "
|
|
"Zamora's patrol car was parked on the ridge. Captain Richard T. Holder "
|
|
"of White Sands Missile Range arrived that evening."
|
|
)
|
|
|
|
|
|
# ────────────── red 1: la abreviatura ya no regala el pase ──────────────
|
|
|
|
def test_el_punto_de_una_abreviatura_no_exime_a_la_palabra_siguiente():
|
|
"""«Lt. Colonel»: RE_ARRANQUE veía el punto de «Lt.» y daba por hecho que
|
|
«Colonel» iba en mayúscula por empezar frase, así que NO lo comprobaba. La
|
|
única palabra capaz de cazar el error era justo la que se saltaba."""
|
|
nombres, _, _ = I.revisa("Blue Book chief Lt. Colonel Hector Quintanilla spoke", ART)
|
|
assert "Colonel" in nombres, nombres
|
|
|
|
|
|
def test_un_punto_de_verdad_sigue_eximiendo():
|
|
"""La excepción no puede cargarse la regla: detrás de un punto de final de
|
|
frase, una mayúscula sí es posicional y no debe marcarse."""
|
|
nombres, _, _ = I.revisa("The object left. Nothing else was found.", ART)
|
|
assert nombres == [], nombres
|
|
|
|
|
|
# ────────────── red 2: comparar por palabra, no por subcadena ──────────────
|
|
|
|
def test_un_token_corto_no_se_esconde_dentro_de_otra_palabra():
|
|
"""«Lt» daba positivo por estar dentro de *built*, *adults* y *consultant*.
|
|
Cuanto más corto el token más fácil colarse — y los tokens cortos son
|
|
justamente las abreviaturas de cargo."""
|
|
assert not I._en_articulo("Lt", ART)
|
|
assert "built" in ART and "consultant" in ART # la trampa sigue ahí
|
|
|
|
|
|
def test_los_plurales_y_genitivos_siguen_valiendo():
|
|
"""La comparación se ata solo por la IZQUIERDA a propósito: el artículo dice
|
|
«Zamora's» y el guion «Zamora», y eso no es contaminación."""
|
|
assert I._en_articulo("Zamora", ART)
|
|
assert I._en_articulo("Quintanilla", ART)
|
|
|
|
|
|
# ────────────── red 3: el cargo se comprueba en FRASE ──────────────
|
|
|
|
def test_un_cargo_falso_se_caza_aunque_sus_palabras_esten_sueltas():
|
|
"""El fallo real no fue una palabra inventada sino una COMBINACIÓN falsa.
|
|
Token a token no se puede ver nunca: hay que comparar la frase entera."""
|
|
assert I.cargos_fuera("Lt. Colonel Hector Quintanilla said so", ART) == \
|
|
["Lt. Colonel Hector Quintanilla"]
|
|
|
|
|
|
def test_el_cargo_correcto_no_salta():
|
|
assert I.cargos_fuera("Major Hector Quintanilla said so", ART) == []
|
|
|
|
|
|
def test_otros_cargos_reales_del_articulo_no_saltan():
|
|
"""Si marcara cargos legítimos, el informe se volvería inservible y se
|
|
dejaría de leer — que es como se pierde el aviso de verdad."""
|
|
for frase in ("Sergeant Sam Chavez reached the site",
|
|
"Captain Richard T. Holder of White Sands",
|
|
"his scientific consultant, Dr. J. Allen Hynek"):
|
|
assert I.cargos_fuera(frase, ART) == [], frase
|
|
|
|
|
|
def test_la_abreviatura_no_cuenta_como_cargo_distinto():
|
|
"""«Lt. Col.» y «Lt Col» son el mismo cargo: si el punto los separase,
|
|
cualquier artículo que abrevie distinto daría falso positivo."""
|
|
art = I.pliega("Lt. Col. Hector Quintanilla ran the program.")
|
|
assert I.cargos_fuera("Lt Col Hector Quintanilla ran it", art) == []
|
|
|
|
|
|
# ────────────── camino de producción: filtra() poda lo justo ──────────────
|
|
|
|
def _guion(rango):
|
|
return {
|
|
"titular": "Socorro UFO Landing, 1964",
|
|
"subtitulo": "Four depressions and no explanation",
|
|
"entradilla": "Sergeant Sam Chavez reached the arroyo first.",
|
|
"cronologia": [
|
|
{"marca": "within 24 hours",
|
|
"texto": f"Blue Book chief {rango} Hector Quintanilla dispatched Hynek."},
|
|
{"marca": "within minutes",
|
|
"texto": "Sergeant Sam Chavez reached the arroyo first."}],
|
|
"paneles": [{"titulo": "The witness", "vinetas": [
|
|
"Zamora's patrol car was parked on the ridge",
|
|
f"{rango} Hector Quintanilla classified it"]}],
|
|
"franja": [],
|
|
}
|
|
|
|
|
|
def test_filtra_tira_solo_las_frases_con_el_cargo_falso():
|
|
inf = {"avisos": [], "caidas": []}
|
|
out = I.filtra(_guion("Lt. Colonel"), ART, inf)
|
|
assert len(out["cronologia"]) == 1, out["cronologia"]
|
|
assert sum(len(p["vinetas"]) for p in out["paneles"]) == 1
|
|
assert out["titular"], "no debía tocar el titular"
|
|
assert any("Colonel" in c for c in inf["caidas"]), inf["caidas"]
|
|
|
|
|
|
def test_filtra_no_toca_nada_si_el_cargo_es_el_bueno():
|
|
"""La otra mitad: un verificador que poda de más es tan inútil como uno que
|
|
no poda — deja el guion cojo y obliga a rehacerlo a mano."""
|
|
inf = {"avisos": [], "caidas": []}
|
|
out = I.filtra(_guion("Major"), ART, inf)
|
|
assert len(out["cronologia"]) == 2, inf["caidas"]
|
|
assert sum(len(p["vinetas"]) for p in out["paneles"]) == 2, inf["caidas"]
|
|
assert inf["caidas"] == [], inf["caidas"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
fallos = 0
|
|
for nombre, fn in sorted(globals().items()):
|
|
if not nombre.startswith("test_"):
|
|
continue
|
|
try:
|
|
fn()
|
|
print(f" ✓ {nombre}")
|
|
except AssertionError as exc:
|
|
fallos += 1
|
|
print(f" ✗ {nombre}: {exc}")
|
|
print(f"\n{'✓ todo pasa' if not fallos else f'✗ {fallos} fallo(s)'}")
|
|
sys.exit(1 if fallos else 0)
|