Skip to main content
Experimental: paquete privado@botmesh/sandbox es privado ("private": true): no está en npm y su API puede cambiar. Úsalo desde un clon del monorepo (pnpm install && pnpm build). Ver Paquetes y versiones.
@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

El comando arranca los nueve simuladores, un bot por canal y una página web (un único HTML estático, sin build). Elige un canal en la barra lateral y escribe como usuario. Puedes enviar archivos (📎), compartir una ubicación (📍), pulsar botones y filas de listas y responder encuestas. Ves las respuestas del bot como las mostraría la plataforma, con los checks de entregado y leído. El inspector de la derecha tiene tres pestañas:
  • 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.
Opciones:

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 a 127.0.0.1, así que la API se protege:
  • Escucha en 127.0.0.1 por defecto. --host lo cambia, y la CLI avisa si la dirección no es de loopback. --host solo mueve el hub: los simuladores siguen siempre en 127.0.0.1, porque, como las APIs de proveedor que imitan, no comprueban ni Host ni token.
  • Toda petición debe llevar una cabecera Host de loopback con el puerto del hub (127.0.0.1, localhost o [::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ía Origin o Sec-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 cabecera x-sandbox-token. La CLI lo imprime al arrancar y el hub lo inserta en la página que sirve. --token (o startSandbox({ token })) lo fija para scripts, y sandbox.token lo 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 CSP sandbox y, salvo imágenes, audio y vídeo normales, como descarga.
Los botones de una encuesta envían un voto nativo en los canales cuyo provider lo convierte en un evento '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:
Lo que puede hacer un usuario simulado (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, el To 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 defecto true).

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.
Interacciones en Discord y TelegramEn Discord (interactionsRequireAck) el core confirma cada interacción por su cuenta: una confirmación diferida (interaction_callback:6) antes de la primera respuesta del handler, poco antes del plazo de 3 s o al terminar el handler, salvo que el handler haya llamado antes a ctx.acknowledge(texto) (interaction_callback:4). En Telegram la confirmación es opcional, así que sin ctx.acknowledge() el botón se queda con el indicador de carga. El sandbox registra cada confirmación como un registro action.

Plataformas autoalojadas en Docker

Algunas plataformas se pueden ejecutar en local: Evolution API, WPPConnect Server, Chatwoot y Mailpit. Sus suites opcionales están en packages/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:
  1. Arranca el stack y llama a start() en el provider (o crea la instancia desde el manager de Evolution en http://127.0.0.1:47811/manager).
  2. Escanea el QR desde WhatsApp → Dispositivos vinculados en un teléfono de pruebas.
  3. Vuelve a ejecutar las suites. Con la instancia en open también funciona el envío. Las suites solo necesitan que la instancia exista.
Nunca apuntes las suites de Docker a un Chatwoot de producción ni a un número de producción emparejado: crean bandejas, webhooks e instancias y los borran al terminar.