@botmesh/sandbox simula las plataformas de mensajería en tu máquina para probar un bot en
todos los canales de extremo a extremo, sin registrar una app, un número ni un webhook público.
El simulador de la terminal reemplaza el provider por uno en memoria. El
sandbox hace lo contrario: conserva el provider real y reemplaza la plataforma. Tu bot
habla por HTTP real con un servidor local que responde como WhatsApp, Telegram o Twilio. Ese
servidor entrega webhooks firmados con un secreto que él mismo elige, así que la verificación de
firmas, el mapeo de errores, la descarga de media y los reintentos pasan por el mismo código que
en producción.
Elige la herramienta según lo que quieras comprobar:
Baileys no tiene simulador aquí: WhatsApp Web es un protocolo binario sobre socket, no una API
HTTP documentada. Prueba los bots de Baileys con
MockProvider y el simulador de la terminal, y
vincula un número de pruebas para la comprobación final.
Canales
Cada simulador es un servidor
node:http pequeño (Discord añade un gateway WebSocket en el mismo
puerto). Cada provider apunta a él con la opción de URL base que ya tenía: graphUrl (Meta,
Messenger, Instagram), apiUrl y gatewayUrl (Discord), baseUrl (Telegram, Gupshup,
HighLevel), apiBaseUrl (Twilio, TikTok). sim.providerConfig() devuelve esa configuración,
incluidas las opciones de descarga de media que permiten http://127.0.0.1.
El chat web
- API requests: cada llamada del provider, con su estado y cualquier diferencia con la forma documentada de la petición.
- Webhooks: cada entrega al bot, con su estado HTTP y el número de intento.
- Faults & user: hace fallar el siguiente envío con un error de la plataforma (límite de tasa, destinatario inválido, fuera de la ventana de 24 h, media demasiado grande, autenticación, error del servidor). Aquí también se cambian el id y el nombre del usuario simulado.
API solo local
La página habla con el hub mediante una pequeña API JSON. Cualquier web que abras en el mismo navegador también podría llegar a127.0.0.1, así que la API se protege:
- Escucha en
127.0.0.1por defecto.--hostlo cambia, y la CLI avisa si la dirección no es de loopback.--hostsolo mueve el hub: los simuladores siguen siempre en127.0.0.1, porque, como las APIs de proveedor que imitan, no comprueban niHostni token. - Toda petición debe llevar una cabecera
Hostde loopback con el puerto del hub (127.0.0.1,localhosto[::1], más los nombres de--allow-host/allowedHosts). Eso frena el DNS rebinding, en el que un dominio hostil pasa a apuntar a 127.0.0.1. Si el navegador envíaOriginoSec-Fetch-Site, tiene que ser el origen del propio hub. - Las llamadas que cambian estado (
POST /api/<canal>/send|fault|status|reset) necesitan además el token de la ejecución en la cabecerax-sandbox-token. La CLI lo imprime al arrancar y el hub lo inserta en la página que sirve.--token(ostartSandbox({ token })) lo fija para scripts, ysandbox.tokenlo devuelve desde código. - Los cuerpos están acotados (32 MiB para
send, 64 KiB en el resto). Un JSON mal formado, campos desconocidos, tipos erróneos o canales que no existen reciben un 4xx con{ "error": "..." }que nombra el problema, y un simulador que no puede expresar una entrada (una ubicación en Discord) responde 422. - La media subida desde la página se sirve con
nosniff, una CSPsandboxy, salvo imágenes, audio y vídeo normales, como descarga.
'poll' (POLL_VOTE_PARSING en @botmesh/sandbox/providers). En los demás
envían el texto de la opción. Los simuladores de Telegram y Discord ya entregan votos nativos
(poll_answer, MESSAGE_POLL_VOTE_ADD); hoy solo el provider de Telegram los interpreta, así
que Telegram recibe un voto nativo y Discord el texto de la opción. Un test falla en cuanto un
provider y el conjunto no coinciden, así que el conjunto se actualiza junto con el provider.
El bot demo (--bot demo, por defecto) responde a hola, menu, ubicacion (envía una
ubicación) y encuesta (envía una encuesta), también en inglés (hi, location, poll), confirma la media y las ubicaciones que recibe, y pasa todo lo demás a aiPlugin
con un MockAIProvider guionizado (sin API key). --bot bakery ejecuta el
ejemplo de la panadería
en los mismos canales.
Para usar tu propio bot, llama a startSandbox() desde código:
En tests
startHarness() arranca el simulador, la entrada de webhooks del bot (@botmesh/server) y un
Bot sobre el provider real:
sim.send(input)): text, media ({ kind, data, mimetype, filename, caption }), location, reply (botón o fila de lista), edit (Telegram,
Messenger), pollVote (Telegram, Discord) y quotedId. Cada mensaje que envía el provider se normaliza
en un OutboundRecord (kind, text, buttons, sections, media, location, template,
poll, action) y conserva la petición original.
Validación contra la especificación
Cada simulador valida las peticiones que recibe contra la forma documentada de la plataforma: campos obligatorios, enumerados, longitudes y cantidades. Por ejemplo, el texto de WhatsApp de 4096 caracteres, 3 botones de respuesta con títulos de 20 caracteres, 10 filas de lista, elTo
de Twilio más From o MessagingServiceSid más un cuerpo, el contenido de 2000 caracteres y las
5 filas de componentes de Discord, y el callback_data de 64 bytes de Telegram. Los esquemas
usan un subconjunto de JSON Schema.
Twilio y Discord publican documentos OpenAPI oficiales con licencia MIT, así que sus
simuladores validan además cada petición de crear mensaje (y las ediciones y los followups de
webhook de Discord) contra fragmentos de twilio-oai y
discord-api-spec. Esos problemas llevan la
etiqueta [openapi]. Los fragmentos están copiados en packages/sandbox/src/specs/ con sus
licencias y commits, así que los tests nunca usan la red. packages/sandbox/scripts/vendor-openapi.mjs los
regenera desde copias descargadas. Meta, Gupshup, HighLevel, TikTok y Telegram no publican una
especificación oficial legible por máquina, así que sus esquemas siguen escritos a mano a partir
de la documentación.
En modo estricto, el predeterminado, una petición inválida recibe el error de validación propio
de la plataforma. Con strict: false se acepta y solo aparece en sim.issues().
Escenarios
Las opciones del simulador cubren el resto:
webhookRetries/webhookRetryDelayMs: reentrega cuando el bot no responde 2xx.mediaTtlMs: las URLs de media entrante caducan.autoStatus: 'read': confirmaciones tras cada mensaje del bot.strict: responde a las peticiones inválidas con el error de validación de la plataforma (por defectotrue).
Errores
sim.fail(kind, { times, match, retryAfterSec }) responde con el formato de la propia
plataforma. El provider debe entonces rechazar con la subclase tipada de ProviderError:
¹ Telegram y Discord no tienen ventana de mensajería: su respuesta más parecida se mapea a
RecipientUnreachableError.
El core reintenta en el sitio un ctx.reply que recibe un Retry-After corto (5 s como
máximo), así que las suites de extremo a extremo usan retryAfterSec: 30 para ver el error.
Plataformas autoalojadas en Docker
Algunas plataformas se pueden ejecutar en local: Evolution API, WPPConnect Server, Chatwoot y Mailpit. Sus suites opcionales están enpackages/sandbox/tests/docker/ y se saltan si no se
definen las variables:
Los contenedores llegan al servidor de webhooks del test por
host.docker.internal. Define
BOTWA_TEST_DOCKER_HOST si tu Docker usa otro nombre. Todos los puertos se publican solo en
127.0.0.1. WPPConnect Server está fijado en 1.3.50: la 1.3.55 (latest en octubre de 2026)
descarta el QR de una sesión iniciada por la API REST y solo llama a URLs de webhook de una lista
permitida (WEBHOOK_ALLOWED_URLS).
Lo que sigue siendo manual
Emparejar un número de WhatsApp real (escanear el QR de Evolution o WPPConnect, o el código de emparejamiento de Baileys) necesita un teléfono, así que sigue siendo manual:- Arranca el stack y llama a
start()en el provider (o crea la instancia desde el manager de Evolution enhttp://127.0.0.1:47811/manager). - Escanea el QR desde WhatsApp → Dispositivos vinculados en un teléfono de pruebas.
- Vuelve a ejecutar las suites. Con la instancia en
opentambién funciona el envío. Las suites solo necesitan que la instancia exista.