# Pruebas de fallos de herramientas de agentes: detecta pronto los reintentos inseguros

Las pruebas de fallos de herramientas de agentes deben centrarse en lo que el agente cree después de que una operación falle, no solo en si la herramienta emitió un error. Una herramienta que informa «la solicitud falló» después de que una escritura remota quizá se haya completado crea un problema mayor que una herramienta que se detiene con una respuesta incompleta. Los agentes planifican sus siguientes pasos basándose en el resultado que reciben.

El camino feliz oculta las decisiones que determinan si una ejecución autónoma es segura: reintentar, solicitar aprobación, rotar el acceso, reparar los datos o detenerse. He visto conjuntos de herramientas con cientos de pruebas correctas que nunca habían provocado una caída de red entre el inicio de un comando remoto y la devolución de su salida. Esos conjuntos no probaban la parte peligrosa.

## La respuesta a un fallo es una entrada para el planificador del agente

Un agente trata el resultado de una herramienta como una evidencia. Si el resultado indica que no hubo cambios, puede reintentar. Si indica que una credencial caducó, puede buscar una vía de recuperación autorizada. Si indica éxito cuando el resultado remoto es desconocido, puede construir varias acciones posteriores sobre una ficción.

Separa los fallos según lo que el llamador puede saber. Esta distinción se confunde constantemente:

- Un rechazo confirmado significa que el servicio remoto recibió la solicitud y la rechazó.
- Un fallo confirmado significa que el servicio remoto devolvió un resultado que indica que no realizó el trabajo solicitado.
- Un resultado incierto significa que el llamador no puede establecer si el sistema remoto realizó el trabajo.
- Un fallo local significa que la herramienta falló antes de poder hacer un intento remoto significativo.

Una conexión rechazada antes de abrirse una sesión TCP suele ser un fallo local. Un HTTP 403 es un rechazo confirmado. Un tiempo de espera de lectura después de enviar un `POST` es incierto, salvo que el servicio remoto ofrezca una forma de consultar la operación. Estas etiquetas deben aparecer en los casos de prueba y en el esquema de resultados de la herramienta. No las escondas en una frase que el agente tenga que interpretar.

Una forma compacta del resultado facilita probar el contrato:

```json
{
  "ok": false,
  "category": "outcome_unknown",
  "operation": "create_deployment",
  "retry": "reconcile_first",
  "correlation_id": "case-ssh-017",
  "message": "Connection closed after the remote command started; remote completion is unknown."
}
```

Los nombres no importan demasiado. La separación sí. `retry: "never"` para un rechazo por permisos y `retry: "reconcile_first"` para una escritura cuyo tiempo de espera se agotó le indican cosas distintas al agente sin convertir todo el mensaje de error en una instrucción.

No devuelvas los errores sin procesar del proveedor como única interfaz. Cambian con frecuencia, suelen incluir texto irrelevante y a veces contienen datos de la solicitud que no deberías entregar a un agente. Conserva el estado, el cuerpo y los encabezados originales en diagnósticos protegidos. Devuelve al llamador un resultado estable y deliberadamente pequeño.

## Construye la matriz alrededor de las operaciones y las evidencias

Una matriz útil cruza cada operación con los modos de fallo que pueden cambiar su significado. Empieza enumerando las herramientas que leen, crean, actualizan, eliminan, activan o ejecutan. Una lectura cuyo tiempo de espera se agota tiene una regla de recuperación distinta de la de un comando que cambia un host de producción.

Usa esta matriz como punto de partida. Sustituye los nombres de las operaciones y los registros esperados por los tuyos, pero no elimines la columna «¿se conoce el efecto remoto?». Esa columna obliga a sacar a la luz los casos incómodos.

| Caso | Operación | Condición inyectada | ¿Se conoce el efecto remoto? | Categoría esperada | Instrucción para el agente |
| --- | --- | --- | --- | --- | --- |
| C01 | leer incidencia | falla la consulta DNS | sí, no se envió ninguna solicitud | local_failure | reintentar dentro de un límite establecido |
| C02 | crear incidencia | el token caducó | sí, fue rechazada | authentication_failed | detenerse y solicitar una recuperación autorizada de la credencial |
| C03 | eliminar versión | permiso denegado | sí, fue rechazada | authorization_denied | no reintentar |
| C04 | leer compilación | JSON contiene `status: 7` | sí, se recibió la respuesta | malformed_response | detenerse e informar de una incompatibilidad de esquema |
| C05 | crear despliegue | la respuesta se retrasa más allá del límite del cliente | no | outcome_unknown | conciliar antes de reintentar |
| C06 | ejecutar reinicio por SSH | el asistente local termina después del inicio remoto | no | outcome_unknown | inspeccionar el estado remoto antes de enviar otro comando |
| C07 | actualizar registro | el servicio devuelve 429 | sí, fue rechazada | rate_limited | esperar según las instrucciones y reintentar si es seguro |

Añade filas para las operaciones que gastan dinero, modifican permisos, rotan credenciales o afectan a un estado compartido. Esas operaciones necesitan más de una fila de tiempo de espera. Prueba un tiempo de espera antes de que los bytes salgan del proceso, después de que salgan los encabezados de la solicitud, después de que el servicio acepte la solicitud y mientras llega el cuerpo de la respuesta. Los puntos exactos de inyección dependen del protocolo, pero agruparlos en un único caso genérico de «tiempo de espera» elimina el comportamiento que necesitas verificar.

Cada fila necesita cuatro comprobaciones:

1. Comprueba la categoría del resultado de la herramienta y la instrucción de reintento.
2. Comprueba la siguiente acción del agente, incluido que no improvise un reintento destructivo.
3. Comprueba el estado remoto o la razón documentada por la que sigue siendo desconocido.
4. Comprueba que el registro de eventos contiene el identificador de correlación y el resultado observado.

Esto requiere más trabajo que comprobar `ok == false`. También detecta los fallos que importan después de que el agente ya haya realizado varias acciones.

## Las credenciales caducadas y las acciones denegadas requieren recuperaciones distintas

Una credencial caducada o revocada demuestra que la autenticación falló. Una acción denegada demuestra que el llamador se autenticó, pero no tiene permiso para esa operación, salvo que el proveedor oculte deliberadamente la diferencia. Tratar ambas situaciones como «fallo de acceso» produce un comportamiento incorrecto del agente.

RFC 9110 define 401 como una solicitud no autenticada y exige que el servidor envíe un desafío `WWW-Authenticate`. Define 403 como una negativa a atender la solicitud, incluso cuando el servidor no explica el motivo. Los proveedores no siempre siguen esta distinción de forma clara, así que prueba la respuesta real del proveedor. Aun así, la herramienta debe traducir las evidencias observadas a categorías separadas cuando pueda hacerlo honestamente.

Haz que la prueba de una credencial caducada use una credencial aceptada durante la configuración y rechazada durante la llamada real. Una cadena falsa que el servicio nunca reconoció solo prueba la rama de credencial no válida. Lo que quieres detectar son las cachés, el código de renovación y los traductores de errores que se comportan de otra forma cuando el token de acceso ya ha caducado.

Un dispositivo sencillo puede expresar ambos casos sin exponer un secreto:

```yaml
cases:
  - id: expired-token
    request:
      method: POST
      path: /v1/releases
    fixture_response:
      status: 401
      headers:
        www-authenticate: Bearer error="invalid_token"
      body: {"error":"token_expired"}
    expect:
      category: authentication_failed
      retry: never
      secret_in_result: false

  - id: denied-release
    request:
      method: POST
      path: /v1/releases
    fixture_response:
      status: 403
      body: {"error":"insufficient_scope"}
    expect:
      category: authorization_denied
      retry: never
      secret_in_result: false
```

La comprobación `secret_in_result` detecta un error que aparece durante una depuración apresurada: el código añade el encabezado de autorización saliente o el objeto de configuración a una excepción. Prueba la salida serializada de la herramienta, la salida de trazas y cualquier transcripción que llegue al agente. La redacción en un registrador no protege a otro registrador.

No hagas que el agente «pruebe otra credencial» salvo que el sistema le proporcione explícitamente una identidad distinta y autorizada. La selección ciega de credenciales puede cruzar un límite de privilegios mientras parece resolver un problema de disponibilidad. Una prueba debe demostrar que un fallo de autenticación detiene la ejecución o la dirige a la vía de recuperación humana aprobada.

## Los datos mal formados necesitan una prueba de contrato, no una prueba del analizador JSON

Los datos mal formados incluyen JSON válido que el código no puede usar de forma segura. La sintaxis no válida es el caso fácil. Los fallos de producción suelen consistir en que un campo cambia de tipo, desaparece un identificador obligatorio, un envoltorio de error sustituye al envoltorio de éxito o una respuesta se trunca después de que un proxy cierre la conexión.

La especificación JSON-RPC 2.0 separa los errores de análisis (`-32700`) de las solicitudes no válidas (`-32600`). Esta separación es útil porque distingue los bytes ilegibles de un mensaje legible que infringe el protocolo. Aplica la misma disciplina a las respuestas de tu dominio: que el análisis funcione no demuestra que la respuesta cumpla el contrato de la herramienta.

Para cada respuesta de proveedor que consumas, escribe dispositivos que infrinjan una sola suposición cada vez:

- Sustituye un ID de cadena por `null`, un número y un objeto.
- Omite un campo que las llamadas posteriores necesiten para conciliar el estado.
- Devuelve un estado de éxito con un cuerpo con formato de error.
- Devuelve un estado de error con un cuerpo HTML o un documento JSON truncado.
- Duplica un elemento o cambia el orden cuando el código selecciona el primero.

Después comprueba el comportamiento exacto. La herramienta debe indicar el campo o la condición de contrato que falla en los diagnósticos protegidos, devolver `malformed_response` al agente y no realizar ninguna mutación posterior basada en valores supuestos.

Un patrón incorrecto habitual parece inofensivo: `response.id || request.id`. Mantiene el flujo en marcha cuando el proveedor omite `id`, pero puede provocar una actualización o eliminación sobre un objeto no relacionado si la identidad de la solicitud y la de la respuesta son distintas. Prueba que la ausencia de identidad en la respuesta detiene la operación. Un flujo fallido cuesta menos que una escritura incorrecta.

Los clientes de herramientas MCP necesitan el mismo cuidado. El formato de resultados de herramientas del Model Context Protocol admite una señal `isError` para indicar un fallo de la herramienta. Úsala cuando la herramienta no pueda completar el trabajo prometido, pero mantén el contenido del resultado lo bastante específico para que el agente elija una rama segura. No disfraces una respuesta ascendente mal formada como un resultado de texto normal que empiece por «Error:». Muchos clientes lo tratarán como una ejecución correcta de la herramienta y dejarán que el agente deduzca el resto.

## Los tiempos de espera son ambiguos cuando empieza una escritura

Un tiempo de espera agotado indica que venció tu límite. No identifica el estado de la operación remota. Parece obvio hasta que un bucle de reintentos convierte silenciosamente una respuesta perdida en una factura duplicada, dos despliegues o un segundo reinicio.

Prueba el comportamiento en el límite donde cambia la certeza. El inyector de fallos o el servicio simulado debe registrar cada etapa:

```text
case=C05 request_id=case-http-005 received=true
case=C05 request_id=case-http-005 mutation_committed=true
case=C05 response_write=delayed
client case=C05 deadline_exceeded=true
```

La comprobación esperada no es «el cliente recibió un tiempo de espera». Es que el cliente devuelva `outcome_unknown`, no emita un segundo `POST` y use una consulta de estado o un mecanismo de idempotencia antes de continuar.

Los tokens de idempotencia solo ayudan cuando la API remota los documenta y los respeta para la operación en cuestión. Pruébalos como una secuencia completa: envía una solicitud con un token único, retrasa la primera respuesta hasta que el llamador abandone, envía el mismo token por la vía de recuperación y verifica que el servicio informe de una única operación lógica. No afirmes que existe idempotencia solo porque has añadido un encabezado que el proveedor ignora.

Para las operaciones sin un endpoint de conciliación ni compatibilidad con idempotencia, indícalo en el resultado de la herramienta. La acción segura puede ser detenerse y pedir a una persona que inspeccione el sistema remoto. Eso no es un fallo de ingeniería. Fingir certeza porque un flujo quiere continuar sí lo es.

Establece límites por fase cuando el cliente lo permita: conexión, escritura de la solicitud, primer byte de respuesta y duración total de la operación. Un único límite grande oculta si el interlocutor nunca aceptó una conexión o si aceptó la escritura y después se quedó bloqueado. Las pruebas no tienen que exponer cada fase al agente, pero los diagnósticos deben ofrecer suficientes detalles para que un operador reproduzca el evento.

## Los comandos remotos interrumpidos deben conservar la incertidumbre

Un comando SSH tiene una ventana de fallo que los desarrolladores de HTTP suelen subestimar. El cliente puede enviar el comando, el shell remoto puede iniciarlo y la conexión puede cerrarse antes de que el llamador reciba el estado de salida. Un fallo del proceso local o una ruta de red perdida no deshace el trabajo que el host remoto ya ha iniciado.

OpenSSH documenta que su cliente devuelve el estado de salida del comando remoto cuando puede obtenerlo. Cuando el transporte se interrumpe antes, el llamador no dispone de ese estado. Prueba este caso deliberadamente en lugar de tratar una salida local distinta de cero como prueba de que el comando remoto falló.

Crea un comando remoto de prueba que registre una marca de inicio, espere, registre una marca de finalización y escriba un resultado reconocible. Después termina el transporte local mientras está esperando. Mantén esta prueba limitada a un host o contenedor aislado que controles.

```sh
# remote command used only in an isolated test environment
id="case-ssh-017"
printf '%s start\n' "$id" >> /tmp/agent-tool-test.log
sleep 20
printf '%s complete\n' "$id" >> /tmp/agent-tool-test.log
```

Ejecuta el comando mediante la misma ruta SSH que usa la herramienta, espera hasta que aparezca la marca de inicio y después termina el asistente local. Cuando haya transcurrido la espera remota, inspecciona el registro. Ejecuta la prueba dos veces: una en la que el proceso remoto termine y otra en la que el sistema remoto lo mate después de la marca de inicio. Ambas producen una interrupción local, pero requieren recuperaciones distintas.

Para los comandos que cambian el estado, diseña un comando de conciliación antes de diseñar un reintento. Un reinicio de servicio puede consultar el tiempo de actividad del proceso o la revisión del despliegue. Una instalación de paquetes puede consultar la versión instalada. Un comando que no pueda conciliarse debe requerir intervención humana explícita después de una interrupción.

Evita fragmentos de shell que oculten una finalización parcial detrás de cadenas `&&` y una salida imprecisa. Emite un ID de operación duradero antes de iniciar la parte que modifica el estado y usa ese ID durante las inspecciones posteriores. Si el entorno remoto no puede conservar ninguna marca, la herramienta no tiene base para decirle a un agente que es seguro reintentar.

## Las denegaciones humanas son un resultado normal, no una prueba rota

Una persona que rechaza una acción debe producir un resultado distinto que cierre limpiamente esa rama. Los equipos suelen probar que aparece una pantalla de aprobación y olvidan probar la ruta de rechazo. Como consecuencia, los agentes reintentan, reformulan la misma solicitud o informan de un fallo de aprobación como si fuera un problema de red.

Prueba la denegación en cada límite de autorización que expongas. Verifica que la herramienta no se conecte al servicio remoto después de una denegación. Verifica que no conserve una aprobación para un proceso posterior o para una acción posterior que requiera una decisión nueva. Verifica que el agente reciba un texto que pueda usar sin tratar el rechazo como una invitación a buscar una alternativa.

El almacén de Sallyport deniega todas las acciones mientras está bloqueado, y sus aprobaciones de sesión y por llamada permiten probar estas decisiones sin colocar credenciales en el proceso del agente. Esta separación es útil porque un almacén bloqueado, una sesión rechazada y un uso por llamada denegado pueden detener una operación por motivos diferentes.

La fatiga de aprobaciones es un fallo de prueba por derecho propio. Si una lectura inofensiva genera avisos repetidos durante una ejecución normal, la gente aprobará sin leer. Si una llamada destructiva hereda accidentalmente una aprobación amplia, las personas nunca verán el punto de decisión que esperaban. Prueba el número, el momento y el alcance de los avisos, además de su presencia.

Usa un agente de prueba que intente una acción aprobada, una acción denegada y una acción después de que el proceso termine. La llamada final detecta si el estado de aprobación se filtra más allá de la sesión prevista. No simules esto solo cambiando un booleano en memoria; inicia un proceso nuevo para que la prueba comparta el ciclo de vida que realmente usan tus usuarios.

## Los registros deben explicar qué ocurrió sin exponer el acceso

Un registro de fallos útil permite reconstruir la causalidad: qué ejecución del agente intentó qué operación, qué identificador de solicitud usó, qué observó el sistema remoto, qué devolvió la herramienta y qué hizo después el agente. No debería tener que contener la credencial que autorizó la llamada.

Registra un evento en cada punto donde pueda cambiar la respuesta. Para una escritura con tiempo agotado, captura la construcción de la solicitud, el inicio del envío, la aceptación remota si el dispositivo de prueba puede informarla, el vencimiento del límite, el intento de conciliación y la clasificación final. Incluye un ID de correlación generado antes de la primera acción de red. No lo derives de un secreto ni lo reutilices en varias operaciones.

Este formato funciona para un arnés de pruebas local:

```json
{"time":"2025-04-12T10:18:03Z","case":"C05","id":"case-http-005","event":"dispatch_started"}
{"time":"2025-04-12T10:18:03Z","case":"C05","id":"case-http-005","event":"remote_committed"}
{"time":"2025-04-12T10:18:08Z","case":"C05","id":"case-http-005","event":"client_timeout"}
{"time":"2025-04-12T10:18:08Z","case":"C05","id":"case-http-005","event":"result","category":"outcome_unknown"}
```

Después, tu comprobación puede comparar los registros del cliente y del dispositivo mediante `id`. Si el dispositivo indica `remote_committed` y la herramienta dice `confirmed_failure`, la prueba debe fallar. Ese desacuerdo revela una afirmación insegura, aunque todas las rutas de código hayan devuelto un objeto de error ordenado.

Para una pista resistente a manipulaciones, prueba también la verificación. Sallyport proyecta los registros de sesión y actividad desde un registro de auditoría cifrado y encadenado mediante hashes, y `sp audit verify` comprueba la cadena sin conexión y sin necesitar una clave del almacén. Una prueba de fallos debe añadir una secuencia de eventos conocida, verificarla, modificar una copia de un registro y comprobar que la verificación falla en la copia modificada.

No pongas cuerpos completos de solicitudes en los registros normales por defecto. Los datos de las solicitudes suelen contener datos personales, código fuente o tokens incluidos por un llamador descuidado. Registra el nombre de la operación, la clasificación del destino, el ID de correlación, la categoría del resultado y una referencia de diagnóstico protegida. Amplía la recopilación solo en un entorno de pruebas controlado donde sepas qué contienen los dispositivos.

## Prueba el comportamiento de recuperación del agente, no solo el adaptador

Las pruebas unitarias demuestran que un adaptador traduce 403 a `authorization_denied`. No demuestran que un agente se detenga después de recibirlo. Ejecuta un conjunto pequeño de pruebas de extremo a extremo con una instrucción de agente determinista y un servicio remoto simulado que exponga el registro de eventos inyectados.

Asigna a cada ejecución una tarea limitada y un límite explícito. Por ejemplo: crear un registro, leerlo y añadir una nota. Retrasa la respuesta de creación después de confirmar el registro. El comportamiento correcto del agente es inspeccionar mediante el ID de correlación o el token de idempotencia antes de intentar una segunda creación. La prueba debe fallar si crea otro registro, aunque al final complete la tarea.

Mantén estable la instrucción del agente para este conjunto. Si cambias al mismo tiempo la instrucción, el contrato de la herramienta, el comportamiento del dispositivo y la versión del modelo, una prueba fallida te dirá muy poco. Registra la transcripción de la herramienta y la siguiente llamada del agente y compáralas con las transiciones permitidas:

```text
create -> outcome_unknown -> lookup_by_request_id -> found -> attach_note
create -> outcome_unknown -> create
```

La primera transición solo está permitida si la consulta confirma la creación original. La segunda es un fallo. Esta comprobación mediante una máquina de estados resulta más útil que juzgar si la respuesta final en prosa sonaba razonable.

Ejecuta los casos deterministas de la matriz con cada cambio en el código de la herramienta, los esquemas de resultados, la gestión de autorizaciones o la lógica de reintentos. Repite los casos de interrupción en una infraestructura aislada porque la planificación afecta a sus resultados. Cuando aparezca un incidente nuevo, añade la reproducción más pequeña a la matriz antes de corregirlo. De lo contrario, la misma vía de recuperación atractiva pero incorrecta volverá durante la próxima refactorización.

El estándar de una herramienta no es que siga avanzando después de cada fallo. El estándar es que diga la verdad sobre lo que sabe, deje evidencias y se niegue a convertir la incertidumbre en una segunda acción destructiva.
