Cambios en el esquema de una API: pruebas de contrato para agentes más seguros
Los cambios en el esquema de una API pueden modificar las decisiones de un agente de IA sin provocar ningún error. Usa pruebas de contrato para comprobar campos, valores predeterminados, significado de las respuestas y reintentos.

Un agente puede convertir un cambio menor de una API en un error de acción real más rápido que un cliente manejado por una persona. Un campo de respuesta renombrado puede hacer que seleccione todos los recursos en lugar de uno. Un valor predeterminado nuevo puede ampliar una consulta. Un objeto de estado modificado puede parecer una autorización para reintentar, y el reintento puede repetir un cargo, un despliegue o una solicitud de eliminación.
Lo peligroso es que estos fallos suelen parecer normales en la monitorización convencional. El proveedor devuelve HTTP 200. El cliente no se bloquea. El agente ofrece una explicación plausible. Las pruebas de contrato deben comprobar el significado que el agente atribuye a los datos de la API, no solo si el JSON se puede analizar.
Las decisiones del agente convierten la compatibilidad en una propiedad de seguridad
Un cliente de API usado por una persona suele fallar de forma visible cuando cambia una respuesta. Un botón muestra una tabla vacía, aparece un error en el formulario y alguien investiga antes de realizar la siguiente acción. Los agentes suelen convertir una respuesta directamente en otra solicitud. Pueden hacer esa conversión varias veces antes de que alguien vea una transcripción.
Imagina un agente de limpieza que llama a GET /projects?state=inactive, lee el campo owner de cada elemento y solicita aprobación antes de archivar los proyectos que están fuera de una lista permitida. Más tarde, el proveedor cambia owner por owner_id, pero conserva el endpoint y el código de estado antiguos. Un analizador permisivo convierte el owner ausente en una cadena vacía. Si la regla del agente dice que un propietario vacío significa que el proyecto no está asignado, prepara una solicitud de archivo mucho más amplia.
No es un fallo de autenticación ni de instrucciones. Es un fallo de interpretación en el límite de la API. El control correctivo también debe estar ahí.
Trata cualquier campo que influya en una de estas decisiones como parte de un contrato de seguridad:
- sobre qué objeto puede actuar el agente
- si un objeto puede recibir la acción
- el alcance, el importe o el destino de la acción
- si una acción anterior terminó, falló o necesita un reintento
- si el agente debe pedir autorización a una persona antes de continuar
La diferencia importa porque la compatibilidad de transporte es mucho más débil que la compatibilidad de comportamiento. Un servicio puede conservar su endpoint, método, esquema de autenticación y sintaxis JSON, y aun así romper la decisión que sigue a la respuesta. Los equipos suelen llamar a eso un cambio compatible porque los SDK existentes todavía pueden deserializarlo. Para un cliente autónomo, esa etiqueta puede ser peligrosamente incompleta.
Crea un mapa pequeño de cada valor de respuesta que llegue a un selector de acciones, una decisión de autorización o una rama de reintento. No empieces por todas las propiedades de una especificación de API enorme. Empieza por los valores cuya interpretación incorrecta cambia lo que el agente puede hacer.
Una diferencia de esquemas no demuestra la compatibilidad de comportamiento
Las herramientas de comparación de esquemas detectan cambios útiles, pero no pueden decirte si un cambio es seguro para un agente concreto. Comparan declaraciones. Un agente depende de significados que a menudo quedan fuera de esas declaraciones.
Supón que un proveedor cambia limit de un parámetro opcional con un valor predeterminado implícito de 100 a otro parámetro opcional con un valor predeterminado implícito de 1000. Una comparación estándar de OpenAPI puede no mostrar ningún cambio en las propiedades obligatorias. Sin embargo, un agente que omite limit ahora puede inspeccionar diez veces más objetos y enviar una acción por lotes sobre todos ellos.
La especificación OpenAPI describe un Schema Object como un superconjunto del vocabulario de JSON Schema y señala que sus propiedades proporcionan información para las cargas de solicitudes y respuestas. Eso resulta útil para documentar y validar. No indica que state: "pending" permita un reintento ni que omitir limit siga limitando el resultado a 100. Esas son afirmaciones del flujo de trabajo, y tus pruebas deben expresarlas con claridad.
Del mismo modo, la anotación default de JSON Schema no obliga a un validador o cliente a insertar un valor. Muchos desarrolladores dan por hecho que sí. La documentación de JSON Schema trata default como datos de anotación, no como una orden que modifica una instancia. Si tu seguridad depende de un valor, haz que el cliente lo envíe explícitamente y prueba la solicitud exacta que sale. No esperes que una anotación del esquema corrija una omisión.
Usa una herramienta de comparación como señal de alarma. Después, clasifica cada cambio detectado según la ruta de acción a la que puede afectar:
- Un identificador renombrado puede cambiar el objetivo seleccionado.
- Añadir un valor a un enumerado puede enviar el analizador a una rama no probada.
- Cambiar un valor predeterminado puede alterar el alcance sin modificar el código de la solicitud.
- Cambiar una representación puede invertir el significado de finalización o fallo.
También importa lo contrario. Una herramienta puede informar de un campo descriptivo nuevo que ningún agente lee. Merece una revisión, pero no congelar la producción. La revisión de compatibilidad mejora cuando sigue los datos hasta una decisión, en lugar de tratar cada línea del esquema como igual de arriesgada.
Las pruebas de contrato deben fijar las solicitudes y las decisiones
Una prueba de contrato útil tiene dos partes: verifica la solicitud que el agente envía realmente y, después, verifica la acción que propone tras leer la respuesta del proveedor. Probar solo una de las dos deja un punto ciego importante.
Para los valores predeterminados, captura una solicitud HTTP real en un servidor de prueba local o en un entorno aislado del proveedor. El siguiente ejemplo en Python usa httpx.MockTransport para inspeccionar una solicitud saliente. Evita el fallo habitual por el que un cliente depende silenciosamente de un valor predeterminado del proveedor para una operación destructiva.
import httpx
seen = []
def handler(request: httpx.Request) -> httpx.Response:
seen.append({
"method": request.method,
"path": request.url.path,
"query": dict(request.url.params),
})
return httpx.Response(200, json={"items": []})
client = httpx.Client(transport=httpx.MockTransport(handler))
response = client.get(
"https://api.example.test/projects",
params={"state": "inactive", "limit": "100"},
)
assert response.status_code == 200
assert seen == [{
"method": "GET",
"path": "/projects",
"query": {"state": "inactive", "limit": "100"},
}]
La aserción importante no es la respuesta 200. Es limit explícito. Si una refactorización elimina ese parámetro, la prueba falla antes de que el nuevo valor predeterminado del proveedor pueda ampliar la selección.
Después, prueba la decisión. Mantén separada la función de planificación del agente del código que realiza la llamada HTTP, para que la prueba pueda inspeccionar una acción propuesta sin ejecutarla.
from dataclasses import dataclass
@dataclass
class ArchivePlan:
project_ids: list[str]
requires_approval: bool
def plan_archives(items: list[dict], allowed_owners: set[str]) -> ArchivePlan:
targets = []
for item in items:
owner = item.get("owner")
if owner is None:
raise ValueError("provider response lacks owner")
if item["state"] == "inactive" and owner in allowed_owners:
targets.append(item["id"])
return ArchivePlan(targets, requires_approval=bool(targets))
items = [
{"id": "p17", "state": "inactive", "owner": "team-a"},
{"id": "p18", "state": "inactive", "owner": "team-b"},
]
plan = plan_archives(items, {"team-a"})
assert plan.project_ids == ["p17"]
assert plan.requires_approval is True
Esta prueba toma una decisión que el código permisivo suele evitar: la ausencia de owner provoca un error. Para un campo que selecciona una acción, es mejor detenerse de forma segura. Devolver una cadena vacía, None o un valor alternativo supuesto puede mantener el proceso en marcha, pero sustituye un fallo de integración detectable por un plan potencialmente inseguro.
Mantén los datos de prueba lo bastante pequeños como para que una persona revisora pueda entender por qué aparece cada elemento. Un conjunto con mil objetos puede parecerse a producción, pero oculta la condición que querías proteger.
Los campos renombrados necesitan un comportamiento de error explícito
Un campo renombrado debería detener una ruta de acción, salvo que hayas decidido admitir ambos nombres durante una migración definida. El tratamiento silencioso de valores alternativos parece resistente en una demostración y crea significados no revisados en producción.
La peor versión se parece a esto:
owner = item.get("owner", "")
if owner not in blocked_owners:
archive(item["id"])
Cuando desaparece owner, todos los elementos parecen tener un propietario que no está bloqueado. El analizador hizo exactamente lo que le pidió el código. La persona ingeniera probablemente quería evitar un KeyError. Esa pequeña comodidad convirtió la ausencia de datos en permiso para actuar.
Escribe pruebas para las tres situaciones: el campo esperado, el campo antiguo si existe una promesa temporal de compatibilidad y la ausencia de ambos. La tercera prueba debe indicar si el agente se detiene, omite el objeto o pide una aclaración. Para la identidad del objetivo, el estado de autorización y el alcance de la acción, detenerse suele ser la respuesta correcta.
Si admites un alias, deja clara su prioridad y haz que sea temporal:
def read_owner(item: dict) -> str:
if "owner" in item:
return item["owner"]
if "owner_id" in item:
return item["owner_id"]
raise ValueError("owner identity missing")
Este código también necesita una prueba para datos contradictorios. Si llegan ambos campos y no coinciden, no elijas uno en silencio. Devuelve un error y deja que el proveedor resuelva la ambigüedad. Una capa de compatibilidad debe conservar un significado antiguo conocido, no inventar un criterio para datos incoherentes.
A veces los equipos sostienen que el análisis permisivo protege frente a la evolución del proveedor. Protege frente a adiciones inofensivas cuando ignoras campos desconocidos. No protege frente a campos ausentes que gobiernan una acción. En esos casos se necesita el comportamiento contrario: aceptar información adicional por defecto, pero rechazar la ausencia de un significado obligatorio.
Los valores predeterminados y las omisiones requieren pruebas separadas
Una propiedad omitida, un valor null explícito y un valor explícito son tres solicitudes distintas. Los agentes suelen mezclarlas porque los serializadores de propósito general también lo hacen.
Un constructor de solicitudes puede omitir dry_run cuando su valor interno es None. El proveedor podría interpretar la omisión como false. En una versión posterior, podría cambiar el significado de la omisión a «usar la configuración de la cuenta», y esa configuración podría ser false para una organización y true para otra. El código del agente no cambió, pero sí la acción.
Clasifica las opciones que influyen en una decisión en una de estas dos categorías. Para las opciones cuyo comportamiento seguro se conoce, envía siempre el valor. Para las opciones que requieren una decisión de la persona operadora, exige esa decisión antes de construir la solicitud. Evita una tercera categoría llamada «dejar que el servidor decida» en acciones destructivas o visibles externamente.
Prueba la serialización con una tabla de casos exactos. Lo importante es inspeccionar la representación transmitida, no solo el objeto del lenguaje antes de serializarlo.
| Intención | Representación saliente | Significado esperado para el proveedor |
|---|---|---|
| Leer proyectos inactivos | state=inactive&limit=100 | Una selección limitada |
| Simular un archivo | {"dry_run": true} | No se archiva nada |
| Archivar un proyecto | {"project_ids":["p17"],"dry_run": false} | Solo puede cambiar p17 |
| No hay una decisión de la persona operadora sobre el modo | La solicitud se rechaza localmente | No se envía nada al proveedor |
Sé igual de estricto con la paginación. Una respuesta que añade next_cursor puede tentar al agente a obtener automáticamente todas las páginas. Eso puede ser razonable para un informe y temerario para un plan de acciones. Prueba tanto el número máximo de objetos que puede considerar el planificador como la condición que permite obtener una segunda página. Un cursor es un mecanismo de continuación, no un consentimiento para ampliar el alcance sin límite.
Los valores predeterminados del proveedor también importan en las respuestas. Si una API empieza a omitir archivable cuando es false, un código como if item.get("archivable", True) cambia su comportamiento en la dirección insegura. Para un campo que concede permiso, usa una comparación explícita como item.get("archivable") is True. Es menos elegante y mucho más fácil de auditar.
La validación de respuestas debe conservar el significado, no solo la forma
La validación de respuestas debe distinguir los datos mal formados de los datos desconocidos pero inofensivos. Una rigidez generalizada rompe los clientes cuando los proveedores añaden campos. Una permisividad generalizada convierte la falta de pruebas en una suposición.
Define un modelo de respuesta reducido alrededor de los valores que alimentan la siguiente acción. Para cada valor, especifica el tipo, los estados permitidos y si su ausencia detiene el flujo de trabajo. Un identificador de proyecto necesita algo más que string: el agente necesita un identificador estable y no vacío que coincida con el identificador enviado después en la solicitud de archivo. Un estado necesita algo más que string: el agente necesita un estado enumerado con una acción documentada para cada miembro.
Por ejemplo, este analizador gestiona un cambio en la representación del estado sin concederse permiso para reintentar:
ALLOWED_STATES = {"queued", "running", "succeeded", "failed"}
def retryable(job: dict) -> bool:
status = job.get("status")
if status not in ALLOWED_STATES:
raise ValueError(f"unknown job status: {status!r}")
return status == "failed" and job.get("retry_allowed") is True
Si el proveedor cambia status de una cadena a un objeto como {"phase":"failed"}, este código se detiene. La interrupción es correcta hasta que alguien decida cómo se asigna la nueva representación al flujo de trabajo anterior. Si el proveedor añade cancelled, detenerse también es correcto hasta que el equipo decida si la cancelación es terminal, permite reintentar o requiere la intervención de una persona.
No conviertas cada valor desconocido de un enumerado en una emergencia cuando solo afecta a una pantalla de lectura. La respuesta debe corresponder a la acción. Un agente de informes puede etiquetar un estado desconocido y continuar. Un agente que va a reintentar un trabajo de facturación o eliminar un recurso debe detenerse antes de actuar sobre un estado que no entiende.
Prueba también las relaciones entre campos. Una respuesta puede ser válida desde el punto de vista estructural y, aun así, contener una combinación contradictoria, como status: "succeeded" y retry_allowed: true. La validación del esquema normalmente no expresa todas las invariantes del negocio. Una prueba de contrato debería comprobar que un trabajo completado no crea ningún plan de reintento, independientemente de un booleano incorrecto.
Prueba toda la ruta de acción, no una simulación cómoda
Las pruebas unitarias de los analizadores son necesarias, pero no demuestran que el agente desplegado envíe la solicitud prevista a través de su ruta real de credenciales y ejecución. Las bibliotecas de serialización, los envoltorios, los adaptadores de herramientas y el middleware de reintentos cambian el comportamiento de formas que una llamada directa a una función no puede revelar.
Ejecuta en CI un servidor de contratos local que registre solicitudes y devuelva datos de prueba versionados. Configura las herramientas del agente para apuntar a ese servidor. La prueba debe ejecutar una instrucción realista, esperar el plan propuesto o el registro de acción y comprobar la secuencia registrada: método, ruta, consulta, encabezados que sea seguro inspeccionar, cuerpo y número de acciones.
No pongas secretos reales en este entorno. Usa una credencial de prueba sin autoridad y verifica que el agente nunca la reciba en sus instrucciones, resultados de herramientas, textos de excepción ni trazas. Una prueba que registre encabezados de solicitud sin cuidado puede reproducir la exposición de credenciales que pretendía evitar.
Para los agentes que realizan llamadas HTTP o SSH externas mediante Sallyport, la ruta de acción puede mantener las credenciales fuera del agente y conservar un resultado inspeccionable. Ese límite no corrige una interpretación incorrecta de una respuesta, por lo que debes ejecutar los contratos de esquema y decisión antes de permitir la acción externa.
Incluye datos de error, no solo respuestas de referencia. Introduce las condiciones exactas que los proveedores producen durante los cambios: un campo de selección ausente, un miembro de enumerado nuevo, un resultado vacío con un cursor de continuación, un cambio de tipo de contenido y una respuesta 200 que contiene un objeto de error. Un cuerpo de error con estado 200 es especialmente común en las API antiguas. Si tu analizador supone que toda respuesta 200 tiene la forma de éxito, puede fabricar un plan vacío o reintentar una solicitud que ya terminó correctamente.
Al probar reintentos, comprueba el comportamiento de idempotencia. Haz que el servidor de contratos devuelva primero una respuesta que agote el tiempo de espera después de registrar la solicitud, y luego una segunda respuesta para el reintento. La prueba debe demostrar que el cliente envía un token de idempotencia cuando la API lo admite o que se detiene y pide confirmación cuando no puede saber si la primera acción terminó. Reintentar una lectura suele ser inofensivo. Reintentar una transferencia, un correo electrónico, un despliegue o una solicitud de eliminación no lo es.
Las puertas de lanzamiento deben bloquear las rupturas semánticas
Ejecuta comparaciones de esquemas, pruebas de contrato del proveedor y pruebas de decisiones del agente como parte del flujo de cambios, tanto para quienes producen la API como para quienes consumen agentes. Un proceso de lanzamiento que las ejecuta solo después del despliegue convierte las pruebas en documentación del incidente.
Para un cambio del proveedor, exige un registro de revisión que responda a cuatro preguntas concretas: qué supuestos del consumidor cambian, cuál era el comportamiento anterior, durante cuánto tiempo se admitirán ambos comportamientos y qué dato de prueba demuestra el nuevo comportamiento. Es menos trabajo que discutir una regresión en producción con registros incompletos.
Para un cambio del agente, ejecuta el conjunto existente de datos de prueba del proveedor antes de fusionarlo. Si ahora el agente usa un campo nuevo, añade datos de prueba para su ausencia y para valores fuera del caso habitual. Los cambios en la redacción de las instrucciones también pueden modificar los argumentos de las herramientas, así que prueba la llamada que emite el agente completo, en lugar de suponer que el planificador seguirá eligiendo los mismos parámetros.
Los datos de prueba versionados facilitan la revisión. Guarda un identificador como projects-list-v1 junto con el par esperado de solicitud y respuesta. Cuando un proveedor introduzca deliberadamente projects-list-v2, conserva el dato anterior hasta que termine la política de migración. No sobrescribas el JSON antiguo y llames actuales a las pruebas. Perderás la prueba de la compatibilidad que eliminaste.
Una puerta útil informa de los fallos con lenguaje operativo. «Falta el campo owner esperado, se detuvo la planificación del archivo» indica a la persona revisora qué ocurrió. «ValidationError en la ruta items.0» es mejor que nada, pero obliga a reconstruir el riesgo durante el lanzamiento.
Las pantallas de aprobación no pueden corregir un plan engañoso
La aprobación humana sigue siendo un buen control para las acciones externas, pero llega demasiado tarde si el agente construyó el plan equivocado a partir de un contrato modificado. Una persona que ve «Archivar 847 proyectos inactivos» puede rechazarlo. Una persona que ve «Archivar el proyecto p17» no puede saber si p17 procede de un campo owner ausente, de un valor predeterminado ampliado o de un analizador que confundió cancelled con failed.
Haz que los registros de aprobación incluyan las entradas de decisión que merecen revisión humana: identificadores de objetivos, cantidad, modo solicitado y campos de respuesta que hicieron que el objetivo pudiera recibir la acción. Mantén el registro compacto. Volcar el JSON completo sobre quien aprueba traslada la tarea de análisis del código a una persona cansada.
Conserva por separado una traza que permita a una persona ingeniera reconstruir la acción. Captura la respuesta del proveedor o un resumen protegido de ella, la versión del analizador, la versión del dato de prueba del contrato, la solicitud generada y la respuesta resultante. Un registro de auditoría resistente a manipulaciones ayuda después de los hechos, pero debe apuntar al límite de decisión, no limitarse a registrar que ocurrió una llamada HTTP.
La próxima vez que un equipo de API diga que un cambio de respuesta es meramente cosmético, pídele que ejecute el conjunto de contratos del agente. Si falla, el cambio lleva asociado un comportamiento. Corrígelo antes de que una respuesta 200 cortés se convierta en una acción insegura.
FAQ
¿Puede un pequeño cambio de nombre en un campo de la API volver inseguro a un agente de IA?
Sí. El cambio de nombre de un campo puede hacer que el agente tome una ruta alternativa, trate un valor ausente como seguro o envíe solicitudes de seguimiento mal formadas. La API puede devolver 200 en todo momento, por lo que las comprobaciones habituales de disponibilidad no detectan el fallo.
¿Bastan las instantáneas de respuestas de la API para garantizar la seguridad de un agente?
Las pruebas de instantáneas ayudan a detectar cambios, pero suelen generar diferencias ruidosas y dicen poco sobre el comportamiento. Úsalas para una revisión deliberada y añade después aserciones para los pocos campos y relaciones que controlan una acción.
¿Debo usar contratos impulsados por el consumidor o pruebas de OpenAPI?
Un contrato impulsado por el consumidor suele encajar mejor cuando el flujo de trabajo de un agente depende de una API de proveedor concreta. Registra lo que necesita el consumidor, mientras que el esquema del proveedor sigue siendo útil para documentar toda la superficie pública.
¿Qué diferencia hay entre un campo JSON ausente y null para un agente?
Un campo ausente significa que el productor no envió información. Un campo con valor null significa que el productor envió el campo e indicó que no existe ningún valor. Los agentes no deberían tratar ambos estados como equivalentes salvo que el contrato lo establezca explícitamente.
¿Las pruebas de contrato deberían rechazar campos de respuesta desconocidos?
Por lo general, no. Los campos desconocidos suelen aparecer cuando un proveedor añade datos útiles, y rechazarlos vuelve frágiles a los clientes. Recházalos solo cuando aceptar un campo no reconocido pueda alterar una acción sensible o cuando la respuesta deba usar un vocabulario de comandos cerrado.
¿Cómo pruebo un cambio en el valor predeterminado de una API?
Trata un nuevo valor predeterminado como un cambio de comportamiento siempre que afecte al alcance, la paginación, los permisos, los costes, las eliminaciones o los envíos. Prueba la solicitud sin ese campo, porque ahí el valor predeterminado del proveedor toma el control.
¿Cómo puedo probar la ruta real de la API que usa un agente?
Prueba la secuencia real más pequeña: la solicitud del agente, la inyección de credenciales o el límite de autorización, la solicitud al proveedor, la respuesta del proveedor, el analizador y la acción propuesta. Simular solo la llamada final a la API no revela los errores de serialización o interpretación que ocurren entre esas etapas.
¿La validación de JSON Schema garantiza un comportamiento seguro del agente?
No. La validación del esquema confirma que la carga tiene una forma permitida, pero no demuestra que el agente haya elegido la cuenta correcta, interpretado bien un estado o limitado la acción a los objetos previstos. Añade aserciones para esas decisiones.
¿Qué debe hacer un equipo de API antes de eliminar un campo que usan los agentes?
Mantén disponible el comportamiento anterior durante el periodo de migración publicado, emite señales claras de obsolescencia y ejecuta ambas versiones del contrato en CI mientras los clientes migran. No reutilices silenciosamente el nombre de un campo para darle otro significado.
¿La aprobación humana puede compensar unos contratos de API defectuosos?
La aprobación puede detener una llamada concreta, pero no puede hacer que una solicitud engañosa resulte comprensible para quien la aprueba. Ejecuta las comprobaciones del esquema antes de que la llamada llegue a una persona y muestra detalles de aprobación que identifiquen el objetivo y el alcance.