Un respaldo de búsqueda sin credencial se omite en vez de romper la carga
Python / calidad (push) Successful in 3s
Python / tests (push) Successful in 11s
YAML / yaml (push) Successful in 4s

El CI, apenas empezó a ejecutarse de verdad, hizo fallar dos de sus tres
trabajos. La causa es un defecto de diseño, no del CI: la config del agente
declara a Brave como respaldo, y la validación exigía la credencial para poder
*leer* el archivo. Como la config vive en el repositorio y la credencial no, un
clon limpio quedaba sin poder cargar su propia configuración. Mis pruebas en
Gigastar no lo veían porque acá el archivo .env existe.

Ahora se distingue por rol, que es lo que corresponde:

- El primario sin credencial sigue siendo error fatal. Sin él no queda ninguna
  búsqueda en pie, y descubrirlo en la primera consulta real es tarde.
- Un respaldo sin credencial se cae de la cadena y el primario sigue andando.
  Degradarse es la respuesta correcta: tener red de seguridad es mejor que no
  tenerla, pero no tenerla es mejor que no arrancar.

Omitir no es esconder. La propiedad respaldos_omitidos deja el motivo a la
vista, y build_backend emite un aviso al construir la cadena, que es el punto
por el que pasa cualquier entrypoint que use búsqueda.

Verificado escondiendo .env y corriendo la suite y el validador como lo haría
un clon limpio: 124 tests en verde con credencial y sin ella.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 07:47:53 -03:00
parent 03dadc93da
commit 59b7c3acb6
4 changed files with 111 additions and 31 deletions
+41 -10
View File
@@ -193,7 +193,7 @@ def test_el_primario_no_necesita_credenciales():
assert isinstance(primero, DuckDuckGoBackend)
def test_searxng_exige_url():
def test_searxng_como_primario_exige_url():
with pytest.raises(ValueError, match="searxng_url"):
SearchConfig(backend="searxng")
@@ -307,22 +307,53 @@ def test_el_fallback_repetido_no_se_duplica():
assert cfg.cadena == ["duckduckgo", "brave"]
def test_un_fallback_sin_credencial_falla_al_arrancar():
"""No en la primera consulta real — que es justo cuando el primario ya
falló y el respaldo tiene que funcionar."""
with pytest.raises(ValueError, match="fallback 'brave'"):
SearchConfig(backend="duckduckgo", fallbacks=["brave"])
def test_un_respaldo_sin_credencial_se_omite_en_vez_de_romper():
"""La config vive en el repositorio y las credenciales no.
Exigir la clave para poder *leer* la config dejaba el proyecto sin poder
clonarse: el CI y cualquier máquina nueva fallaban antes de empezar. El
respaldo se cae de la cadena; el primario sigue funcionando.
"""
cfg = SearchConfig(backend="duckduckgo", fallbacks=["brave"])
assert cfg.cadena == ["duckduckgo"]
def test_brave_como_primario_tambien_exige_credencial():
with pytest.raises(ValueError, match="backend 'brave'"):
def test_el_respaldo_omitido_queda_a_la_vista():
"""Omitir no es esconder: quedarse sin red de seguridad tiene que verse."""
cfg = SearchConfig(backend="duckduckgo", fallbacks=["brave"])
assert cfg.respaldos_omitidos == [("brave", "ENLACE_BRAVE_API_KEY")]
def test_construir_la_cadena_avisa_del_respaldo_faltante():
cfg = SearchConfig(backend="duckduckgo", fallbacks=["brave"])
with pytest.warns(RuntimeWarning, match="ENLACE_BRAVE_API_KEY"):
build_backend(cfg)
def test_con_credencial_el_respaldo_si_entra():
cfg = SearchConfig(backend="duckduckgo", fallbacks=["brave"], brave_api_key="x")
assert cfg.cadena == ["duckduckgo", "brave"]
assert cfg.respaldos_omitidos == []
def test_el_primario_sin_credencial_si_es_fatal():
"""Sin primario no queda ninguna búsqueda en pie: eso no se degrada."""
with pytest.raises(ValueError, match="primario 'brave'"):
SearchConfig(backend="brave")
def test_la_config_del_repo_declara_brave_de_respaldo():
def test_la_config_del_repo_carga_sin_credenciales(monkeypatch):
"""Un clon limpio, sin .env, tiene que poder leer la config del repositorio.
Es exactamente lo que rompió en el CI: la validación exigía la clave de
Brave para poder cargar el archivo.
"""
monkeypatch.delenv("ENLACE_BRAVE_API_KEY", raising=False)
monkeypatch.setattr("enlace.config.load.load_dotenv", lambda *a, **k: [])
cfg = load_agent_config()
assert cfg.search.backend == "duckduckgo"
assert "brave" in cfg.search.fallbacks
assert "brave" in cfg.search.fallbacks # la intención queda declarada
assert cfg.search.cadena == ["duckduckgo"] # pero no se usa sin credencial
# --- parser de Brave -------------------------------------------------------