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_dataatualizado 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: new → qualified → (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):
-
qualified_datasobrescrevendo em vez de mergear — corrigido empackages/engine/src/services/leads.ts:updateLeadQualification(). Se aparecer de novo, ver se a função está fazendo GET → merge → UPDATE. -
Leads duplicados por agent_id — quando FIT_ROUTING redireciona, o router copia
qualified_data + namepro lead do agente alvo antes de responder. Verrouter-classifier.tsno blocoif (fitMap && stickyQualifiedData). -
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).
-
Escalação não chamada — Gemini ignora
reportarse a instrução está só no fim do prompt. Fix: adicionar bloco "PRIORIDADE MÁXIMA" no topo do prompt com regra dura. -
Nome do lead não aparece em conversao/próximo agente — depende do router copiar
namejunto comqualified_data(ver fix #2).buildLeadContextsó injeta selead.nameestiver 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 emagent_logsschema - Dispensável pra: typos em prompt, troca de variável por admin, deploy só de admin UI