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
74 lines
3.2 KiB
Python
74 lines
3.2 KiB
Python
"""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.")
|