@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
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.
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 mismosenderId 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:
- ids no vacíos y que no parezcan
undefined/null/NaN/[object Object]; - que concuerden
from/chatId/chat.id,sender/senderId/senderInfo.idyref.chatId; - un
ChatKindválido y coherente conisGroup/isBroadcast, y que un chat nunca cambie de kind; - por persona: un único
senderId, un únicochatIdde DM y los ids/kind esperados; - que dos personas distintas no compartan
senderId.
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 sutests/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
- Simulador en la terminal: prueba el bot a mano
- Sandbox de plataformas: prueba un provider real de extremo a extremo contra un simulador local de su plataforma
- Inspeccionar y validar flujos