# Testes de falhas de ferramentas de agentes: identifique novas tentativas inseguras desde cedo

Os testes de falhas de ferramentas de agentes devem se concentrar no que o agente conclui depois que uma operação falha, não apenas em verificar se a ferramenta emitiu um erro. Uma ferramenta que informa «a solicitação falhou» depois de uma gravação remota que pode ter sido concluída cria um problema pior do que uma ferramenta que para com uma resposta incompleta. Os agentes planejam suas ações com base no resultado que recebem.

O caminho feliz esconde as decisões que determinam se uma execução autônoma é segura: repetir a operação, solicitar aprovação, trocar o acesso, reparar os dados ou parar. Já vi conjuntos de ferramentas com centenas de testes aprovados que nunca haviam provocado uma queda de rede entre o início de um comando remoto e o retorno da saída. Esses conjuntos não testavam a parte perigosa.

## Uma resposta de falha é uma entrada para o planejador do agente

Um agente trata o resultado de uma ferramenta como evidência. Se o resultado indicar que nenhuma alteração ocorreu, o agente poderá repetir a operação. Se indicar que uma credencial expirou, o agente poderá buscar um caminho de recuperação autorizado. Se indicar sucesso quando o resultado remoto for desconhecido, o agente poderá basear várias ações posteriores em uma ficção.

Separe as falhas pelo que o chamador consegue saber. Essa distinção é constantemente confundida:

- Uma recusa confirmada significa que o serviço remoto recebeu a solicitação e a rejeitou.
- Uma falha confirmada significa que o serviço remoto retornou um resultado informando que não executou o trabalho solicitado.
- Um resultado incerto significa que o chamador não consegue determinar se o lado remoto executou o trabalho.
- Uma falha local significa que a ferramenta falhou antes de conseguir fazer uma tentativa remota significativa.

Uma conexão recusada antes da abertura de uma sessão TCP costuma ser uma falha local. Um HTTP 403 é uma recusa confirmada. Um timeout de leitura depois do envio de um `POST` é incerto, a menos que o serviço remoto ofereça uma forma de consultar a operação. Esses rótulos devem aparecer nos casos de teste e no esquema de resultados da ferramenta. Não os esconda em uma frase que o agente precise interpretar.

Um formato compacto de resultado torna o contrato testável:

```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."
}
```

Os nomes não importam tanto. A separação importa. `retry: "never"` para uma recusa de permissão e `retry: "reconcile_first"` para uma gravação que sofreu timeout dizem coisas diferentes ao agente sem transformar a mensagem de erro inteira em um prompt.

Não retorne erros brutos do provedor como única interface. Eles mudam, costumam incluir texto irrelevante e às vezes contêm dados da solicitação que você não deveria entregar ao agente. Preserve o status, o corpo e os cabeçalhos originais em diagnósticos protegidos. Retorne ao chamador um resultado estável e deliberadamente pequeno.

## Monte a matriz em torno das operações e das evidências

Uma matriz útil cruza cada operação com os modos de falha que podem mudar seu significado. Comece listando as ferramentas que leem, criam, atualizam, excluem, disparam ou executam algo. Uma leitura que sofre timeout tem uma regra de recuperação diferente da de um comando que altera um host de produção.

Use esta matriz como ponto de partida. Troque os nomes das operações e os registros esperados pelos seus, mas não remova a coluna «efeito remoto conhecido». Essa coluna força os casos desconfortáveis a aparecerem.

| Caso | Operação | Condição injetada | Efeito remoto conhecido? | Categoria esperada | Instrução para o agente |
| --- | --- | --- | --- | --- | --- |
| C01 | ler problema | falha na consulta DNS | sim, nenhuma solicitação enviada | local_failure | repetir dentro de um limite definido |
| C02 | criar problema | token expirado | sim, rejeitado | authentication_failed | parar e solicitar recuperação autorizada da credencial |
| C03 | excluir release | permissão negada | sim, rejeitado | authorization_denied | não repetir |
| C04 | ler build | JSON contém `status: 7` | sim, resposta recebida | malformed_response | parar e informar incompatibilidade de esquema |
| C05 | criar implantação | resposta atrasada além do prazo do cliente | não | outcome_unknown | reconciliar antes de repetir |
| C06 | executar reinicialização por SSH | auxiliar local encerrado depois do início remoto | não | outcome_unknown | inspecionar o estado remoto antes de outro comando |
| C07 | atualizar registro | serviço retorna 429 | sim, rejeitado | rate_limited | aguardar conforme a instrução e repetir se for seguro |

Adicione linhas para as operações que gastam dinheiro, alteram permissões, trocam credenciais ou afetam um estado compartilhado. Essas operações precisam de mais de uma linha para timeout. Teste um timeout antes de os bytes saírem do processo, depois que os cabeçalhos da solicitação saírem, depois que o serviço aceitar a solicitação e enquanto o corpo da resposta chegar. Os pontos exatos de injeção dependem do protocolo, mas reduzi-los a um único caso genérico de «timeout» elimina o comportamento que você precisa verificar.

Cada linha precisa de quatro asserções:

1. Verifique a categoria do resultado da ferramenta e a instrução de nova tentativa.
2. Verifique a próxima ação do agente, inclusive se ele não improvisa uma repetição destrutiva.
3. Verifique o estado remoto ou o motivo documentado para ele continuar desconhecido.
4. Verifique se o histórico de eventos contém o ID de correlação e o resultado observado.

Isso dá mais trabalho do que verificar `ok == false`. Também captura as falhas que importam depois que o agente já executou várias ações.

## Credenciais expiradas e ações negadas exigem recuperações diferentes

Uma credencial expirada ou revogada comprova que a autenticação falhou. Uma ação negada comprova que o chamador foi autenticado, mas não tem permissão para a operação, a menos que o provedor oculte deliberadamente essa distinção. Tratar ambos como «falha de acesso» produz um comportamento ruim do agente.

A RFC 9110 define 401 como uma solicitação não autenticada e exige que o servidor envie um desafio `WWW-Authenticate`. Ela define 403 como uma recusa em atender à solicitação, mesmo quando o servidor não informa o motivo. Os provedores nem sempre seguem essa distinção de forma clara, então teste a resposta real do provedor. Ainda assim, sua ferramenta deve mapear honestamente as evidências observadas para categorias separadas quando isso for possível.

Faça o teste de credencial expirada usar uma credencial aceita durante a configuração e rejeitada na chamada real. Uma string falsa que o serviço nunca reconheceu testa apenas o ramo de credencial inválida. Você quer detectar caches, código de atualização e mapeadores de erro que se comportam de modo diferente depois que um token de acesso expira.

Uma fixture simples pode expressar os dois casos sem expor um segredo:

```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
```

A asserção `secret_in_result` captura um erro que aparece durante uma depuração apressada: o código anexa o cabeçalho de autorização enviado ou o objeto de configuração a uma exceção. Teste a saída serializada da ferramenta, a saída de rastreamento e qualquer transcrição que chegue ao agente. A redação em um logger não protege outro logger.

Não faça o agente «tentar outra credencial» a menos que o sistema ofereça explicitamente uma identidade distinta e autorizada para isso. A escolha cega de credenciais pode atravessar um limite de privilégio enquanto parece resolver um problema de disponibilidade. Um teste deve provar que uma falha de autenticação interrompe a execução ou a encaminha para o caminho aprovado de recuperação humana.

## Dados malformados exigem um teste de contrato, não um teste do parser JSON

Dados malformados incluem JSON válido que seu código não consegue usar com segurança. Sintaxe inválida é o caso fácil. Falhas de produção costumam chegar como um campo que muda de tipo, um identificador obrigatório que desaparece, um envelope de erro que substitui o envelope de sucesso ou uma resposta truncada depois que um proxy encerra a conexão.

A especificação JSON-RPC 2.0 separa erros de análise (`-32700`) de solicitações inválidas (`-32600`). Essa divisão é útil porque distingue bytes ilegíveis de uma mensagem legível que viola o protocolo. Aplique a mesma disciplina às respostas do seu domínio: o parser ter sido bem-sucedido não prova que a resposta cumpre o contrato da ferramenta.

Para cada resposta de provedor que você consome, escreva fixtures que violem uma suposição por vez:

- Substitua um ID de string por `null`, um número e um objeto.
- Omita um campo que chamadas posteriores da ferramenta exigem para a reconciliação.
- Retorne um status de sucesso com um corpo no formato de erro.
- Retorne um status de erro com um corpo HTML ou um documento JSON truncado.
- Duplique um item ou altere a ordem quando seu código seleciona o primeiro item.

Depois, verifique o comportamento exato. A ferramenta deve identificar o campo ou a condição de contrato que falhou nos diagnósticos protegidos, retornar `malformed_response` ao agente e não executar nenhuma mutação posterior baseada em valores presumidos.

Um padrão ruim comum parece inofensivo: `response.id || request.id`. Ele mantém o fluxo em andamento quando um provedor omite `id`, mas pode causar uma atualização ou exclusão em um objeto não relacionado se a identidade da solicitação e a identidade da resposta forem diferentes. Teste se a ausência da identidade na resposta interrompe a operação. Um fluxo interrompido custa menos do que uma gravação incorreta.

Clientes de ferramentas MCP precisam do mesmo cuidado. O formato de resultado de ferramenta do Model Context Protocol oferece um sinal `isError` para uma falha no nível da ferramenta. Use-o quando a própria ferramenta não conseguir concluir o trabalho prometido, mas mantenha o conteúdo do resultado específico o suficiente para que o agente escolha um ramo seguro. Não disfarce uma resposta malformada do upstream como um resultado de texto normal que começa com «Error:». Muitos clientes tratarão isso como uma execução bem-sucedida da ferramenta e deixarão o agente deduzir o restante.

## Timeouts são ambíguos depois que uma gravação começa

Um timeout informa que seu prazo expirou. Ele não identifica o estado da operação remota. Isso parece óbvio até que um loop de novas tentativas transforme silenciosamente uma resposta perdida em uma fatura duplicada, duas implantações ou uma segunda reinicialização.

Teste o comportamento do timeout no limite em que a certeza muda. Seu injetor de falhas ou serviço falso deve 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
```

A asserção esperada não é «o cliente recebeu um timeout». É que o cliente retorne `outcome_unknown`, não envie um segundo `POST` e use uma consulta de status ou um mecanismo de idempotência antes de prosseguir.

Tokens de idempotência ajudam apenas quando a API remota os documenta e respeita para a operação em questão. Teste-os como uma sequência completa: envie uma solicitação com um token único, atrase a primeira resposta até que o chamador desista, envie o mesmo token pelo caminho de recuperação e depois verifique se o serviço informa uma única operação lógica. Não afirme que há idempotência só porque você adicionou um cabeçalho que o provedor ignora.

Para operações sem endpoint de reconciliação ou suporte a idempotência, informe isso no resultado da ferramenta. A ação segura pode ser parar e pedir que uma pessoa inspecione o sistema remoto. Isso não é uma falha de engenharia. Fingir certeza porque um fluxo quer continuar é.

Defina timeouts por fase quando o cliente oferecer esse suporte: conexão, gravação da solicitação, primeiro byte da resposta e duração total da operação. Um único prazo longo esconde se o par nunca aceitou uma conexão ou se aceitou a gravação e depois parou de responder. Seus testes não precisam expor todas as fases ao agente, mas seus diagnósticos precisam ter detalhes suficientes para que um operador reproduza o evento.

## Comandos remotos interrompidos devem preservar a incerteza

Um comando SSH tem uma janela de falha que desenvolvedores HTTP costumam subestimar. O cliente pode enviar o comando, o shell remoto pode iniciá-lo e então a conexão pode ser encerrada antes que o chamador receba o status de saída. Uma falha do processo local ou uma rota de rede perdida não desfaz o trabalho que o host remoto já iniciou.

O OpenSSH documenta que seu cliente retorna o status de saída do comando remoto quando consegue obtê-lo. Quando o transporte é interrompido antes disso, o chamador não tem esse status. Teste esse caso deliberadamente, em vez de tratar uma saída de processo local diferente de zero como prova de que o comando remoto falhou.

Crie um comando remoto de teste que registre um marcador de início, aguarde, registre um marcador de conclusão e grave um resultado reconhecível. Depois, encerre o transporte local enquanto a espera estiver em andamento. Mantenha tudo restrito a um host ou contêiner isolado que você controla.

```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
```

Execute o comando pelo mesmo caminho SSH usado pela ferramenta, aguarde o aparecimento do marcador de início e então encerre o auxiliar local. Depois que a espera remota terminar, inspecione o registro. Execute o teste duas vezes: uma em que o processo remoto é concluído e outra em que o lado remoto o encerra depois do marcador de início. Ambas produzem uma interrupção local, mas exigem recuperações diferentes.

Para comandos que alteram o estado, crie um comando de reconciliação antes de criar uma nova tentativa. Uma reinicialização de serviço pode consultar o tempo de atividade do processo ou a revisão da implantação. Uma instalação de pacote pode consultar a versão instalada. Um comando que não pode ser reconciliado deve exigir tratamento humano explícito depois de uma interrupção.

Evite trechos de shell que escondam uma conclusão parcial atrás de cadeias `&&` e de uma saída vaga. Emita um ID de operação persistente antes do início da parte que altera dados e use esse ID em inspeções posteriores. Se o ambiente remoto não puder manter nenhum marcador, a ferramenta não terá base para informar ao agente que é seguro repetir a operação.

## Recusas humanas são resultados normais, não testes quebrados

Uma pessoa que recusa uma ação deve produzir um resultado distinto que encerre esse ramo da ação de forma limpa. As equipes costumam testar se uma tela de aprovação aparece e esquecem de testar o caminho de recusa, deixando os agentes repetir a ação, reformular a mesma solicitação ou informar uma falha de aprovação como se fosse um problema de rede.

Teste a recusa em cada limite de autorização exposto. Verifique se a ferramenta não se conecta ao serviço remoto depois de uma recusa. Verifique se ela não mantém uma aprovação para um processo posterior ou para uma ação posterior que exige uma nova decisão. Verifique se o agente recebe uma linguagem que possa usar sem tratar a recusa como um convite para encontrar uma solução alternativa.

O cofre do Sallyport nega todas as ações enquanto está bloqueado, e suas aprovações de sessão e por chamada tornam essas decisões testáveis sem colocar credenciais no processo do agente. Essa divisão é útil porque um cofre bloqueado, uma sessão recusada e um uso por chamada negado podem interromper uma operação por motivos diferentes.

A fadiga de aprovação é uma falha de teste por si só. Se uma leitura inofensiva gerar pedidos repetidos durante uma execução normal, as pessoas aprovarão sem ler. Se uma chamada destrutiva herdar acidentalmente uma aprovação ampla, as pessoas nunca terão o ponto de decisão esperado. Teste o número, o momento e o escopo dos pedidos, além da presença deles.

Use um agente de teste que tente uma ação aprovada, uma ação negada e uma ação depois que o processo sair. A chamada final captura um estado de aprovação que vaza além da sessão pretendida. Não simule isso apenas alternando um Booleano na memória. Inicie um processo novo para que o teste compartilhe o ciclo de vida usado pelos seus usuários.

## Os registros devem explicar o que aconteceu sem expor acesso

Um registro de falha útil permite reconstruir a causalidade: qual execução do agente tentou qual operação, qual identificador de solicitação usou, o que o sistema remoto observou, o que a ferramenta retornou e o que o agente fez em seguida. Ele não deve precisar conter a credencial que autorizou a chamada.

Registre um evento em cada ponto em que a resposta pode mudar. Para uma gravação com timeout, capture a construção da solicitação, o início do envio, a aceitação remota se a fixture puder informá-la, a expiração do prazo, a tentativa de reconciliação e a classificação final. Inclua um ID de correlação gerado antes da primeira ação de rede. Não o derive de um segredo nem o reutilize entre operações.

Este formato de registro funciona em um ambiente de testes 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"}
```

Sua asserção pode comparar os registros do cliente e da fixture por `id`. Se a fixture informar `remote_committed` e a ferramenta disser `confirmed_failure`, faça o teste falhar. Essa discordância revela uma afirmação insegura, mesmo que todos os caminhos do código tenham retornado um objeto de erro organizado.

Para um histórico resistente a adulteração, teste também a verificação. O Sallyport projeta seus registros de sessão e atividade a partir de um log de auditoria criptografado e encadeado por hash, e `sp audit verify` verifica a cadeia offline sem precisar de uma chave do cofre. Um teste de falha deve adicionar uma sequência conhecida de eventos, verificá-la, alterar uma cópia do registro e confirmar que a verificação falha na cópia alterada.

Não coloque corpos completos de solicitações nos registros comuns por padrão. Os dados da solicitação costumam conter dados pessoais, código-fonte ou tokens inseridos por um chamador descuidado. Registre o nome da operação, a classificação do alvo, o ID de correlação, a categoria do resultado e uma referência a um diagnóstico protegido. Amplie a coleta apenas em um ambiente de testes controlado, no qual você saiba o que as fixtures contêm.

## Teste o comportamento de recuperação do agente, não apenas o adaptador

Testes unitários comprovam que um adaptador mapeia 403 para `authorization_denied`. Eles não comprovam que um agente para depois de receber esse resultado. Execute um pequeno conjunto de testes de ponta a ponta com uma instrução determinística para o agente e um serviço remoto falso que exponha o log de eventos injetados.

Dê a cada execução uma tarefa limitada e um limite explícito. Por exemplo: criar um registro, lê-lo novamente e depois anexar uma nota. Atrase a resposta de criação depois de confirmar o registro. O comportamento correto do agente é consultar pelo ID de correlação ou pelo token de idempotência antes de tentar uma segunda criação. O teste deve falhar se ele criar outro registro, mesmo que conclua a tarefa no final.

Mantenha o prompt do agente estável nesse conjunto. Se você mudar o prompt, o contrato da ferramenta, o comportamento da fixture e a versão do modelo ao mesmo tempo, uma falha dirá pouco. Registre a transcrição da ferramenta e a próxima chamada do agente, depois compare-as com as transições permitidas:

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

A primeira transição só é permitida se a consulta confirmar a criação original. A segunda é uma falha. Essa verificação por máquina de estados é mais útil do que julgar se a resposta textual final pareceu sensata.

Execute casos determinísticos da matriz a cada alteração no código das ferramentas, nos esquemas de resultados, no tratamento de autorização ou na lógica de novas tentativas. Execute repetidamente os casos de interrupção em uma infraestrutura isolada, porque o agendamento afeta o resultado. Quando surgir um novo incidente, adicione a menor reprodução à matriz antes de corrigi-lo. Caso contrário, o mesmo caminho de recuperação atraente e errado voltará na próxima refatoração.

O padrão para uma ferramenta não é continuar avançando depois de toda falha. O padrão é dizer a verdade sobre o que sabe, deixar evidências e se recusar a transformar incerteza em uma segunda ação destrutiva.
