ci: pinea anthropic y corre la suite DENTRO de la imagen
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m6s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m6s
Tres piezas de un mismo agujero: ningún test de este repo había visto nunca las
librerías que ejecuta el bot.
1. **El pin.** `anthropic>=0.40.0` era el ÚNICO suelo del fichero — todo lo demás
ya estaba pinneado exacto, y medido con `pip freeze` dentro del pod, todo lo
pinneado cuadra. Ese suelo dejó entrar la 1.2.0, una versión MAYOR, sin un
commit: quitó `temperature` de `messages.create` y rompió el reintento del SEO
(32f0b2c). 1.2.0 es lo que ya corre, así que el pin no cambia producción:
la escribe. Evidencia de compatibilidad del propio pod: convive con
httpx 0.28.1, pydantic 2.13.4 y aiohttp 3.14.1.
2. **La suite en la CI, dentro de la imagen construida.** `Dockerfile.test` parte
del tag recién publicado, así que los tests ven EXACTAMENTE lo que se
despliega. Va antes de actualizar los manifiestos: si falla, no se despliega.
`tests/` no hace falta copiarlo, el runtime ya hace `COPY . .`.
`TELEGRAM_BOT_TOKEN` de mentira porque es obligatorio y en la imagen no hay
`.env` — comprobado que la suite entera pasa sólo con eso.
3. **El barrido, que cierra la clase y no el caso.** `tests/test_sdk_contract.py`
lee con `ast` TODAS las llamadas a `messages.create` del árbol y compara sus
kwargs con la firma del SDK instalado. Comprobar sólo el sitio arreglado
habría dejado los otros ocho a ciegas.
⚠️ Los dos guardianes nuevos NO pueden fallar en un portátil con anthropic
0.102.0: ahí `temperature` sigue en la firma. Verificados a mano dentro del pod
contra 1.2.0 — inyectando la avería en una copia aislada, el barrido la caza; con
el código real, las nueve llamadas pasan. Es el pin + la CI lo que los convierte
en pruebas y no en gestos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EHakatofHeJAzdXL26Q5bq
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""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.")
|
||||
Reference in New Issue
Block a user