Saltar al contenido principal
@botmesh/core/testing te deja probar un bot sin WhatsApp, sin teléfono y sin red. Funciona con cualquier test runner; los ejemplos usan Vitest.

Un primer test

Deja tus handlers y flujos en una función que recibe el bot, así el bot real y los tests registran exactamente lo mismo:
expectReply rechaza con un TestKitError legible cuando la respuesta no llega, así que un test que falla te dice qué envió el bot en su lugar.

createTestBot y MockProvider

createTestBot(config?) crea un Bot ya arrancado sobre un MockProvider. Para probar el módulo del bot que despliegas, pásale su fábrica defineBot en su lugar: await createTestBot(createBot) la llama con el MockProvider y arranca el bot que devuelve (lanza si la fábrica construyó el bot sobre otro provider). chat es un DM (user@s.whatsapp.net): sus helpers simulan lo que hace el usuario y comprueban lo que respondió el bot. Cada helper send*/press* cede una macrotarea antes de resolver, así que el despacho ya ha empezado. Los helpers lanzan TestKitError con un mensaje accionable. expectReply y expectNoReply sondean con timers reales: con vi.useFakeTimers() avánzalos (vi.advanceTimersByTimeAsync) o deja setTimeout/setInterval sin simular. También puedes manejar el mock directamente: provider.incoming(text, { from, sender, ... }), provider.incomingMedia(kind, bytes, opts), provider.interaction(data, { kind }) (pulsar botones y listas), provider.incomingLocation(location, opts), provider.reaction(emoji, { to }) y provider.call({ id, from, status }) (para los triggers CALL). provider.journal guarda en orden los mensajes e interacciones entrantes y los envíos (con su ref), acotado por maxJournal (10 000 por defecto). provider.acks registra cada confirmación de interacción. new MockProvider({ interactionsRequireAck: true }) simula un transporte con plazo estricto de confirmación (Discord), en el que el core confirma cada interacción por su cuenta; pasa provider.interaction(data, { respondBy }) para fijar el plazo. editMessage, deleteMessage y sendDraft son métodos de verdad (se registran en edits, deleted y drafts) que puedes sobrescribir en una subclase. Se exponen según new MockProvider({ streaming, supports }): edit y delete cuando streaming no es 'none', drafts con 'draft'. Uno desactivado vale undefined, así que el bot lo trata como no soportado; pasa supports: { edit: true } para activarlo o supports: { edit: false } para simular un transporte sin él (también oculta la versión de una subclase).

Varios usuarios y grupos

Cada vista lleva chatId, senderId, isGroup y displayName. Las vistas de un mismo chat comparten el cursor de respuestas (en un grupo las respuestas no son por remitente), así que los participantes leen las respuestas del chat en orden, mientras las conversaciones siguen siendo por participante (conversationScope: 'participant'). En un DM, chat.as(userId) es otro DM. provider.journal guarda los eventos que emitió cada vista, listos para assertStableIdentity.

Snapshots de conversación

chat.transcript({ name?, ids?, timestamps?, scope?, as? }) devuelve una transcripción dorada con la misma forma que produce recordTranscript: se parsea, se redacta (redactTranscript) y se reproduce (assertTranscript) sin cambios. Los ids y los offsets t se omiten por defecto para que el snapshot sea estable; scope: 'all' intercala todos los chats que vio el mock y as: 'text' lo pinta una línea por turno. Las reacciones no son turnos de la transcripción.

Matchers de Vitest

@botmesh/core/testing/vitest exporta los matchers y amplía el tipo Matchers de vitest (.not funciona). vitest es una peer dependency opcional: en tiempo de ejecución no importa nada de ella. Los matchers son síncronos: espera con expectReply y después comprueba. MockProvider usa siempre un from estable. Para probar cambios de identidad y entregas hostiles, usa el kit de caos.

Kit de caos: entrega adversaria

ChaosProvider envuelve cualquier IProvider (normalmente MockProvider) y entrega su tráfico de forma adversaria: duplicados, reordenación, latencia, envíos fallidos, desconexiones y alias de identidad. Es determinista para una seed dada.
Con cualquier provider: const chaos = withChaos(miProvider, { seed: 1, duplicates: 1 }); new Bot({ provider: chaos, lang: 'es' }). Controles manuales: chaos.disconnect({ reason, downtimeMs }) (sin downtimeMs queda caído hasta chaos.reconnect()), chaos.replay(n) (un reintento de webhook explícito), chaos.flush() y chaos.settle(). Para las aserciones: chaos.log (lo inyectado) y chaos.delivered (lo que llegó al bot). Para tiempos exactos usa vi.useFakeTimers() y vi.advanceTimersByTimeAsync(). Como el dedupe de entrada está activado por defecto, un bot sometido a duplicates y replay debería seguir respondiendo una sola vez a cada mensaje.

Contrato de identidad para providers

Todo provider debe emitir el mismo senderId para la misma persona, use el alias que use el transporte (ver el contrato de identidad en IProvider). Estas funciones revisan una lista de eventos emitidos:
Qué comprueba:
  • ids no vacíos y que no parezcan undefined/null/NaN/[object Object];
  • que concuerden from/chatId/chat.id, sender/senderId/senderInfo.id y ref.chatId;
  • un ChatKind válido y coherente con isGroup/isBroadcast, y que un chat nunca cambie de kind;
  • por persona: un único senderId, un único chatId de DM y los ids/kind esperados;
  • que dos personas distintas no compartan senderId.
Opciones: requireRefChatId, rejectSuspiciousIds, distinctHumans. assertStableIdentity lanza un IdentityContractError con violations. Hay un ejemplo real en packages/baileys-provider/tests/identity-contract.test.ts.

Contratos de providers

Tres comprobaciones más mantienen coherentes todos los canales (ver el contrato común de los canales). Cada provider integrado las ejecuta en su tests/channel-contract.test.ts.
checkInboundContract(events, { now?, maxFutureSkewMs?, minTimestamp? }) devuelve las violaciones (timestamp-missing, timestamp-unit, timestamp-future, respond-by, body, mentions, message-type); assertInboundContract lanza un InboundContractError. checkCapabilityContract / assertCapabilityContract hacen lo mismo con las capacidades (CapabilityContractError). MockProvider cumple ambos: emite ms epoch, y sus flags typing/readReceipts siguen a la existencia de un método typing()/markRead() (una subclase o un test que añade uno recibe el flag). checkPresenceWhileDisconnected / assertPresenceWhileDisconnected informan la regla presence-disconnected cuando typing() o markRead() rechaza sin conexión.

Transcripciones doradas

Para fijar conversaciones completas, graba una (recordTranscript) o léela del log del store (transcriptFromStore), redáctala, commitéala como JSON y reprodúcela en un test con assertTranscript: cualquier respuesta cambiada, ausente o de más falla con un diff esperado frente a real. La misma reproducción puede condicionar un despliegue. Consulta Transcripciones doradas.

Comprobaciones estáticas

eslint-plugin-botmesh detecta restos de la API v1, un runtime.enter() bloqueante, install() async en plugins, triggers vacíos, esperas sin timeout y estado por usuario en variables de módulo antes de ejecutar el código.

Siguientes pasos