Corregir lo que reportó ruff, que nunca se había ejecutado
ruff estaba configurado en pyproject.toml desde el primer commit y jamás se había corrido. Tenía 15 hallazgos. Dos importan más allá del estilo: - zip() sin strict= trunca en silencio al más corto. En las comparaciones de lotes eso significa que un test podía pasar sin haber comparado todo. Donde los largos deben coincidir ahora es strict=True; donde difieren a propósito (pares consecutivos) queda strict=False, que documenta la intención. - Un import sin usar delataba algo peor: BraveBackend se había escrito sin una sola prueba. Se agregan ocho, contra una respuesta con la forma que devuelve la API, incluidas la limpieza de etiquetas, el caso de límite de tasa —que tiene que distinguirse de 'no respondió'— y que la credencial viaje en la cabecera y nunca en la URL. El respaldo tiene que funcionar justo cuando el primario ya falló; merecía la misma cobertura. El resto es orden de imports, collections.abc y líneas largas.
This commit is contained in:
@@ -7,6 +7,7 @@ no en producción. Nada en esta suite sale a internet.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -322,3 +323,104 @@ def test_la_config_del_repo_declara_brave_de_respaldo():
|
||||
cfg = load_agent_config()
|
||||
assert cfg.search.backend == "duckduckgo"
|
||||
assert "brave" in cfg.search.fallbacks
|
||||
|
||||
|
||||
# --- parser de Brave -------------------------------------------------------
|
||||
#
|
||||
# El respaldo tiene que funcionar justo cuando el primario ya falló, así que su
|
||||
# parser merece la misma cobertura. La respuesta de abajo reproduce la forma que
|
||||
# devuelve la API de Brave.
|
||||
|
||||
_BRAVE = {
|
||||
"web": {
|
||||
"results": [
|
||||
{
|
||||
"title": "Home Assistant",
|
||||
"url": "https://www.home-assistant.io/",
|
||||
"description": "Domótica <strong>libre</strong> y con privacidad.",
|
||||
},
|
||||
{
|
||||
"title": "Home Assistant — Wikipedia",
|
||||
"url": "https://es.wikipedia.org/wiki/Home_Assistant",
|
||||
"description": "Software de automatización del hogar.",
|
||||
},
|
||||
{"title": "Sin url", "url": "", "description": "no debería aparecer"},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _RespuestaFalsa:
|
||||
def __init__(self, payload: dict) -> None:
|
||||
self._cuerpo = json.dumps(payload).encode()
|
||||
|
||||
def read(self) -> bytes:
|
||||
return self._cuerpo
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
return False
|
||||
|
||||
|
||||
def _brave_con(payload, monkeypatch):
|
||||
backend = BraveBackend(api_key="clave-de-prueba")
|
||||
monkeypatch.setattr("urllib.request.urlopen", lambda *a, **k: _RespuestaFalsa(payload))
|
||||
return backend
|
||||
|
||||
|
||||
def test_brave_extrae_titulo_url_y_descripcion(monkeypatch):
|
||||
resultados = _brave_con(_BRAVE, monkeypatch).search("home assistant", 5)
|
||||
assert len(resultados) == 2 # el que no tiene url se descarta
|
||||
assert resultados[0].title == "Home Assistant"
|
||||
assert resultados[0].url == "https://www.home-assistant.io/"
|
||||
|
||||
|
||||
def test_brave_limpia_etiquetas_y_entidades(monkeypatch):
|
||||
resultados = _brave_con(_BRAVE, monkeypatch).search("q", 5)
|
||||
assert "<strong>" not in resultados[0].snippet
|
||||
assert "—" not in resultados[1].title
|
||||
# Los espacios múltiples se colapsan, igual que en el parser de DuckDuckGo.
|
||||
assert " " not in resultados[1].snippet
|
||||
|
||||
|
||||
def test_brave_respeta_el_maximo(monkeypatch):
|
||||
assert len(_brave_con(_BRAVE, monkeypatch).search("q", 1)) == 1
|
||||
|
||||
|
||||
def test_brave_sin_resultados_devuelve_lista_vacia(monkeypatch):
|
||||
assert _brave_con({"web": {"results": []}}, monkeypatch).search("q", 5) == []
|
||||
|
||||
|
||||
def test_brave_respuesta_sin_la_clave_web_no_rompe(monkeypatch):
|
||||
assert _brave_con({"type": "search"}, monkeypatch).search("q", 5) == []
|
||||
|
||||
|
||||
def test_brave_distingue_el_limite_de_tasa(monkeypatch):
|
||||
"""Si el respaldo también está limitado, el mensaje tiene que decirlo: es
|
||||
un diagnóstico distinto de 'no respondió'."""
|
||||
import urllib.error
|
||||
|
||||
def falla_429(*a, **k):
|
||||
raise urllib.error.HTTPError("u", 429, "Too Many Requests", {}, None)
|
||||
|
||||
monkeypatch.setattr("urllib.request.urlopen", falla_429)
|
||||
with pytest.raises(SearchError, match="límite de tasa"):
|
||||
BraveBackend(api_key="x").search("q", 5)
|
||||
|
||||
|
||||
def test_brave_manda_la_credencial_en_la_cabecera(monkeypatch):
|
||||
"""La clave va en X-Subscription-Token, nunca en la URL: una credencial en
|
||||
la query string termina en los registros de todos los intermediarios."""
|
||||
capturado = {}
|
||||
|
||||
def espia(request, *a, **k):
|
||||
capturado["url"] = request.full_url
|
||||
capturado["headers"] = request.headers
|
||||
return _RespuestaFalsa({"web": {"results": []}})
|
||||
|
||||
monkeypatch.setattr("urllib.request.urlopen", espia)
|
||||
BraveBackend(api_key="secreta-123").search("q", 5)
|
||||
assert "secreta-123" not in capturado["url"]
|
||||
assert capturado["headers"]["X-subscription-token"] == "secreta-123"
|
||||
|
||||
Reference in New Issue
Block a user