ghost: resolver los tags por ID, que mandarlos por nombre partía el ES en dos
ALLOWED_TAGS son SLUGS y Ghost casa los tags de un post por NOMBRE. Mandarlos
como {"name": slug} funcionaba en EN por casualidad — allí los tags se llaman
igual que su slug ("military-cases") — y en ES rompía: no hay ningún tag
llamado "casos-militares" (se llama "Casos Militares"), así que Ghost creaba
uno nuevo con ese nombre y, con el slug ya pillado, lo dejaba en
`casos-militares-2`.
Encontrado el 2026-07-29 al preparar el hub: 5 tags duplicados, 7 posts
repartidos entre dos archivos flacos cada uno, y los cinco `-2` ofrecidos a
Google en sitemap-tags.xml. Los datos ya están fusionados en Ghost; esto es
para que no vuelva.
Se resuelve el slug a ID contra la Admin API, que es lo único no ambiguo justo
en el punto donde falla la ambigüedad. Un slug que no exista se DESCARTA con
aviso en vez de crearse: la lista es cerrada, así que no existir significa que
está mal escrita, y crear el tag es exactamente el bug. Si no resuelve nada, o
si Ghost no contesta, se cae al comportamiento anterior antes que publicar un
post sin ninguna categoría.
7 tests nuevos, incluido uno para EN — donde el bug era invisible por la
coincidencia nombre==slug y el resolutor no debe depender de ella.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""El resolutor de tags de Ghost.
|
||||
|
||||
Regresión del 2026-07-29: ALLOWED_TAGS son SLUGS y Ghost casa los tags de un
|
||||
post por NOMBRE. Mandarlos como `{"name": slug}` colaba en EN (los tags se
|
||||
llaman igual que su slug) y en ES creaba duplicados `-2`, partiendo cinco
|
||||
archivos de tag en dos páginas flacas cada uno.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from src.generator.generator import GhostPublisher
|
||||
|
||||
|
||||
class FakeGhost:
|
||||
"""Solo lo que _resolve_tags toca: self.lang y self._admin_get."""
|
||||
|
||||
def __init__(self, respuesta, lang="es"):
|
||||
self.respuesta = respuesta
|
||||
self.lang = lang
|
||||
self.pedido = []
|
||||
|
||||
async def _admin_get(self, query, timeout=15):
|
||||
self.pedido.append(query)
|
||||
return self.respuesta
|
||||
|
||||
resolve = GhostPublisher._resolve_tags
|
||||
|
||||
|
||||
# Los nombres reales del ES: legibles y acentuados, NO iguales a su slug.
|
||||
TAGS_ES = {"tags": [
|
||||
{"id": "id-uap", "slug": "uap", "name": "UAP"},
|
||||
{"id": "id-mil", "slug": "casos-militares", "name": "Casos Militares"},
|
||||
{"id": "id-inv", "slug": "investigacion", "name": "Investigacion"},
|
||||
]}
|
||||
|
||||
|
||||
def corre(fake, slugs):
|
||||
return asyncio.run(FakeGhost.resolve(fake, slugs))
|
||||
|
||||
|
||||
def test_resuelve_slugs_a_id_y_no_manda_nombres():
|
||||
fake = FakeGhost(TAGS_ES)
|
||||
out = corre(fake, ["uap", "casos-militares"])
|
||||
assert out == [{"id": "id-uap"}, {"id": "id-mil"}]
|
||||
# lo que provocaba el bug: ningún `name` sale hacia Ghost
|
||||
assert not any("name" in t for t in out)
|
||||
|
||||
|
||||
def test_preserva_el_orden_porque_el_primero_es_el_primary_tag():
|
||||
fake = FakeGhost(TAGS_ES)
|
||||
assert corre(fake, ["casos-militares", "uap"]) == [{"id": "id-mil"}, {"id": "id-uap"}]
|
||||
|
||||
|
||||
def test_slug_inexistente_se_descarta_en_vez_de_crearse():
|
||||
fake = FakeGhost(TAGS_ES)
|
||||
out = corre(fake, ["uap", "humanoides"])
|
||||
assert out == [{"id": "id-uap"}]
|
||||
|
||||
|
||||
def test_si_no_resuelve_nada_cae_al_tag_por_defecto_por_id():
|
||||
fake = FakeGhost(TAGS_ES)
|
||||
assert corre(fake, ["humanoides", "pentagono"]) == [{"id": "id-inv"}]
|
||||
|
||||
|
||||
def test_ghost_mudo_no_deja_el_post_sin_tags():
|
||||
fake = FakeGhost(None)
|
||||
assert corre(fake, ["uap"]) == [{"name": "uap"}]
|
||||
|
||||
|
||||
def test_en_tambien_resuelve_por_id_aunque_ahi_el_bug_no_se_notara():
|
||||
# En EN nombre == slug, así que el bug era invisible; el resolutor debe
|
||||
# comportarse igual en los dos idiomas y no depender de esa coincidencia.
|
||||
tags_en = {"tags": [{"id": "id-inv-en", "slug": "investigation",
|
||||
"name": "investigation"}]}
|
||||
fake = FakeGhost(tags_en, lang="en")
|
||||
assert corre(fake, ["investigation"]) == [{"id": "id-inv-en"}]
|
||||
|
||||
|
||||
def test_pide_todos_los_tags_no_la_primera_pagina():
|
||||
# Con 12 tags en ES y el límite por defecto de 15 de Ghost hoy cabría, pero
|
||||
# el día que no quepa el fallo sería silencioso: tags que existen tratados
|
||||
# como inexistentes y descartados.
|
||||
fake = FakeGhost(TAGS_ES)
|
||||
corre(fake, ["uap"])
|
||||
assert "limit=all" in fake.pedido[0]
|
||||
Reference in New Issue
Block a user