Pular para conteúdo

Runbook — Testes dry-run de agentes IA

Bateria de testes end-to-end que roda sem enviar mensagem real via Uazapi, sem notificar ClickUp/Chatwoot, sem criar evento no Google Calendar. O pipeline inteiro roda normal (router, Gemini, tools, histórico, FIT_ROUTING) mas os "efeitos colaterais" ficam só no banco (pra sticky/histórico funcionar entre turnos).

Use antes de liberar tráfego real num cliente novo ou após mudança significativa em prompt/router/tool.


Pré-requisitos

Variáveis de ambiente no packages/engine/.env (do monorepo):

ENGINE_URL=https://gita-engine-gita-agents.ewzc9p.easypanel.host
GITA_TEST_SECRET=<secret>
SUPABASE_URL=https://chihuerkisjvmzxejxhs.supabase.co
SUPABASE_SERVICE_KEY=<key>

Regra dura: todo JID usado em teste deve começar com test_ — proteção automática contra uso em produção.


Scripts disponíveis (prontos)

Script Função
scripts/test-chat.ts Dispara webhook com X-Gita-Dry-Run: 1 e faz polling em agent_logs pra coletar respostas, router events e function calls
Endpoint POST /webhook/seed-lead/:agentSlug Cria lead pré-qualificado com qualified_data customizado

Pattern de uso:

cd ~/projetos/gita-agents
set -a && source packages/engine/.env && set +a
export ENGINE_URL="https://gita-engine-gita-agents.ewzc9p.easypanel.host"
npx tsx scripts/test-chat.ts --slug <router-slug> --jid test_<id> --reset \
  "turno 1" "turno 2" "turno 3"

O que dry-run bypass (confirmado)

  • ❌ Envio Uazapi (logs send.dry_run)
  • ❌ Notificação ClickUp/Chatwoot (logs reportar.dry_run)
  • ❌ Google Calendar (tool loga mas não cria evento real)
  • ❌ Delays de digitação (roda rápido)

O que persiste (intencional)

  • messages, leads, agent_logs — essencial pra sticky/histórico funcionar entre turnos
  • qualified_data atualizado pelo Gemini via tools

Bateria padrão — 6 cenários

Cada cenário tem um asserts explícito pra saber se passou. Adaptar nomes/dados ao contexto do cliente.

Cenário 1 — Happy path qualificado

Objetivo: lead qualificado passa pela captação completa, vai pro próximo agente (conversao/vendas/etc), fecha ou agenda.

Asserts: - Sticky routing acontece (mesmo agente em turnos seguintes) - Tool de qualificação é chamada progressivamente (não só no final) - qualified_data termina com todos os campos coletados (merge funciona) - Quando fit é decidido, router redireciona pro agente correto - leads.status evolui: newqualified → (scheduled | downsell_offered)

Cenário 2 — Lead fora do fit (downsell)

Objetivo: lead não tem perfil, captação classifica corretamente e entrega handoff no MESMO turno (não para com "tenho algo importante" pendurado).

Asserts: - qualified_data.fit correto - Mensagem final da captação é completa (contém a oferta do downsell ou equivalente) - Próxima mensagem do lead já entra no agente alvo (router.fit_routing log)

Cenário 3 — FIT_ROUTING puro (lead pré-semeado)

Objetivo: simular lead que já vem qualificado (ex: outro canal encaminhou) e cai direto no agente correto sem passar pela captação.

Passo 1 — seed via REST direto no banco (endpoint seed-lead só aceita phone numérico):

# Cria lead em gita-captacao com qualified_data.fit='gita_agents'
POST /rest/v1/leads
{
  "agent_id": "<captacao_id>",
  "remote_jid": "[email protected]",
  "name": "Ana Seed",
  "status": "qualified",
  "qualified_data": {"fit": "gita_agents", "nome": "Ana", ...}
}

Passo 2 — turno único:

npx tsx scripts/test-chat.ts --slug gita-router --jid test_seed_01 \
  "quero agendar a reunião"

Asserts: - Log router.fit_routing com resolvedSlug=<conversao> no primeiro turno - Agente alvo recebe contexto: chama lead pelo nome, não repete qualificação

Cenário 4 — Escalação prioritária (lead pede humano)

Objetivo: agente identifica pedido explícito de humano (nome conhecido, "quero falar com X") e chama reportar IMEDIATAMENTE, sem insistir em qualificação.

Asserts: - Tool reportar chamada no turno 1 (não no 3 ou 5) - motivo apropriado (solicitou_humano, nome_conhecido, etc.) - contexto começa com prefixo do cliente (ex: [Gita Agents]) - Log reportar.dry_run confirma bypass do ClickUp

Cenário 5 — Negociação de preço

Objetivo: lead tenta negociar antes da etapa certa. Agente deve recusar falar preço e escalar se insistência.

Asserts: - Em ≥2 turnos consecutivos de tentativa de negociação, a resposta não menciona valor, desconto, parcelamento - Após insistência, tool reportar é chamada com motivo negociacao_especial

Cenário 6 — Opt-out no meio da conversa

Objetivo: lead desiste durante qualificação. Agente deve encerrar com classe, sem insistir.

Asserts: - Resposta final é breve, cordial, sem CTA forçado - Tool reportar NÃO é chamada (desistência não é escalação) - Conversa não reengaja automaticamente (se follow-up estiver ativo, ele respeita o opt-out)


Auditoria pós-bateria

Depois de rodar os 6 cenários, rodar auditoria consolidada via REST:

# 1. Erros no pipeline
GET /rest/v1/agent_logs?remote_jid=like.test_*&level=eq.error
# Esperado: 0 resultados

# 2. Estados finais dos leads
GET /rest/v1/leads?remote_jid=like.test_*&select=remote_jid,status,qualified_data,ai_agents(slug)

# 3. Distribuição de eventos (sanidade)
# - Deve ter: pipeline.completed > 0, send.dry_run > 0, function.executed > 0
# - NÃO deve ter: gemini.error recorrente, function.error frequente

Cleanup obrigatório ao final

Apagar todos os registros de teste pra não poluir dashboard e métricas:

DELETE /rest/v1/messages?remote_jid=like.test_*
DELETE /rest/v1/agent_logs?remote_jid=like.test_*
DELETE /rest/v1/leads?remote_jid=like.test_*

Ou via SQL Editor no Supabase:

DELETE FROM messages WHERE remote_jid LIKE 'test_%@s.whatsapp.net';
DELETE FROM agent_logs WHERE remote_jid LIKE 'test_%@s.whatsapp.net';
DELETE FROM leads WHERE remote_jid LIKE 'test_%@s.whatsapp.net';

Bugs recorrentes pra monitorar

Histórico de problemas que apareceram em testes anteriores (2026-04-20):

  1. qualified_data sobrescrevendo em vez de mergear — corrigido em packages/engine/src/services/leads.ts:updateLeadQualification(). Se aparecer de novo, ver se a função está fazendo GET → merge → UPDATE.

  2. Leads duplicados por agent_id — quando FIT_ROUTING redireciona, o router copia qualified_data + name pro lead do agente alvo antes de responder. Ver router-classifier.ts no bloco if (fitMap && stickyQualifiedData).

  3. Transição captacao→downsell travada — captação terminava com "tenho algo importante" sem entregar. Fix no prompt: mensagem final deve ser COMPLETA (entregar oferta do próximo agente no mesmo turno).

  4. Escalação não chamada — Gemini ignora reportar se a instrução está só no fim do prompt. Fix: adicionar bloco "PRIORIDADE MÁXIMA" no topo do prompt com regra dura.

  5. Nome do lead não aparece em conversao/próximo agente — depende do router copiar name junto com qualified_data (ver fix #2). buildLeadContext só injeta se lead.name estiver presente.


Quando rerodar

  • Obrigatório antes de: onboarding de cliente novo (após fase de prompts), mudança estrutural de prompt, refatoração do router, nova tool
  • Recomendado após: ajuste de modelo Gemini, mudança em variables, alteração em agent_logs schema
  • Dispensável pra: typos em prompt, troca de variável por admin, deploy só de admin UI