"""Lo que le pedimos al SDK de Anthropic contra lo que el SDK acepta. Este fichero existe por un fallo concreto: `src/seo/autofill.py` mandaba `temperature=0.0` a `messages.create`, y anthropic 1.2.0 lo quitó de la firma —los parámetros de muestreo se movieron a `output_config`—. La llamada moría con un TypeError que un `except` convertía en aviso, así que el reintento estricto del SEO llevaba meses sin ejecutarse. Nada lo vio: la importación no falla, ningún test toca la API de verdad, y la suite corría en un portátil con anthropic 0.102.0 mientras el pod ejecutaba 1.2.0. Comprobar el caso arreglado no habría bastado. Lo que hace falta es cerrar la CLASE: se leen TODAS las llamadas del árbol con `ast` y se comparan sus kwargs con la firma del SDK instalado. Un kwarg que el SDK ya no acepta falla aquí, en el sitio donde está escrito, sin que nadie tenga que ejercitar ese camino. Junto con el pin de `requirements.txt` y el `pytest` que la CI corre DENTRO de la imagen, esto pasa a ejecutarse contra el SDK que de verdad se despliega — que es lo único que lo convierte en una prueba y no en un gesto. """ from __future__ import annotations import ast import inspect from pathlib import Path import pytest SRC = Path(__file__).resolve().parents[1] / "src" def _llamadas_al_sdk() -> list[tuple[str, int, list[str], bool]]: """(fichero, línea, kwargs, tiene_**kwargs) de cada `*.messages.create(...)`.""" encontradas = [] for fichero in sorted(SRC.rglob("*.py")): arbol = ast.parse(fichero.read_text(encoding="utf-8"), filename=str(fichero)) for nodo in ast.walk(arbol): if not isinstance(nodo, ast.Call): continue fn = nodo.func if not (isinstance(fn, ast.Attribute) and fn.attr == "create" and isinstance(fn.value, ast.Attribute) and fn.value.attr == "messages"): continue nombres = [k.arg for k in nodo.keywords if k.arg is not None] estrella = any(k.arg is None for k in nodo.keywords) encontradas.append((str(fichero.relative_to(SRC)), nodo.lineno, nombres, estrella)) return encontradas def _firma() -> set[str]: from anthropic import AsyncAnthropic return set(inspect.signature( AsyncAnthropic(api_key="test").messages.create).parameters) def test_hay_llamadas_que_revisar(): """Un barrido que no encuentra nada mide una lista vacía y pasa siempre.""" assert len(_llamadas_al_sdk()) >= 5 @pytest.mark.parametrize("fichero,linea,kwargs,estrella", _llamadas_al_sdk(), ids=lambda v: str(v)) def test_cada_llamada_usa_kwargs_que_el_sdk_acepta(fichero, linea, kwargs, estrella): if estrella and not kwargs: # Los kwargs se construyen aparte (p. ej. `_create_kwargs`): esa función # tiene su propio test, que compara el diccionario ya montado. pytest.skip(f"{fichero}:{linea} pasa **kwargs; cubierto en su propio test") firma = _firma() desconocidos = sorted(k for k in kwargs if k not in firma) assert desconocidos == [], ( f"{fichero}:{linea} pasa {desconocidos} a messages.create y el SDK " f"instalado no lo acepta. Arregla la llamada, no relajes el assert.")