Skip to main content

Casos de prueba — WhatsApp

Documentación de los casos de prueba unitarios y de integración del módulo src/app/modules/whatsapp (canal WhatsApp QR sobre Baileys). Fuentes:

  • auth/mongo-auth-state.spec.ts
  • controllers/whatsapp.controller.spec.ts
  • helpers/baileys-logger.spec.ts, incoming-contact.helper.spec.ts, incoming-location.helper.spec.ts, incoming-media.helper.spec.ts, jid.helper.spec.ts, media.helper.spec.ts, quoted-message.helper.spec.ts
  • services/whatsapp-broadcast.wiring.spec.ts, whatsapp-connection.wiring.spec.ts, whatsapp-inbound.service.spec.ts, whatsapp-outbound.dispatcher.spec.ts, whatsapp-session.manager.spec.ts

Resumen: 14 archivos spec, 22 bloques describe (incluidos los anidados), 151 tests (it).

note

baileys es ESM-only y Jest (CJS) no puede cargarlo: todos los specs lo mockean con jest.mock('baileys', ...) antes de cualquier import. Por eso la columna Mocks no repite ese mock salvo cuando aporta (por ejemplo downloadMediaMessage).

mongo-auth-state.spec.ts​

Test de integración con mongodb-memory-server (Mongo real en memoria) y la variable WHATSAPP_SESSION_ENCRYPTION_KEY definida.

makeMongoAuthState — integration (mongodb-memory-server)​

TestEscenarioResultado esperadoMocks
seeds creds via initAuthCreds() on first startPrimer arranque de una conexión sin sesión persistidastate.creds existe (con noiseKey, signedIdentityKey) y en Mongo se guarda el sobre cifrado iv/tag/ciphertext, sin texto plano de las llavesbaileys (initAuthCreds, BufferJSON), Mongo en memoria
round-trips Buffers in creds through saveCreds() and a fresh loadSe llama saveCreds() y se recarga el estado con otra instanciaLas llaves siguen siendo Buffer y son idénticas a las originalesbaileys, Mongo en memoria
keys.set() upserts a value retrievable via keys.get()Se guarda una llave session/peer-1 y se leekeys.get devuelve un Buffer con los mismos bytesMongo en memoria
keys.set() with a null value deletes the rowSe guarda una llave y luego se setea nullkeys.get devuelve undefined y la fila ya no existe en la colecciónMongo en memoria
isolates keys per connectionIdDos conexiones guardan la misma clave shared con distinto valorCada conexión lee su propio valorMongo en memoria
clear() removes the session doc and all its keysSe llama clear() tras guardar una llaveSe elimina el documento de sesión y cuenta 0 llaves de la conexiónMongo en memoria

encryption at rest​

TestEscenarioResultado esperadoMocks
throws a clear error when a stored key value is tampered withSe altera un byte del ciphertext almacenado y se llama keys.getRechaza con error authentication tag verification failedMongo en memoria
throws a clear error when stored creds are legacy plaintext-shaped (no iv/tag/ciphertext)Documento heredado (pre-cifrado) escrito por el driver crudo con creds en texto planomakeMongoAuthState rechaza con not a valid encrypted envelopeMongo en memoria

whatsapp.controller.spec.ts​

WhatsappController​

TestEscenarioResultado esperadoMocks
start(): 404s when the connection does not existfindById devuelve nullNotFoundExceptionconnectionRepository, translateService
start(): 404s when the connection is not a whatsapp-qr channelLa conexión es de canal WEBNotFoundExceptionconnectionRepository
start(): registers the requester BEFORE starting the session, then starts itConexión WhatsApp QR válidaEl orden de llamadas es registerRequester y luego start; responde sessionState QR y el qr crudobroadcastWiring, connectionRepository, sessionManager.start (spy)
logout(): delegates to the session managerConexión válidaLlama sessionManager.logout(connectionId)sessionManager.logout (spy)
status(): falls back to the persisted whatsappConfig when no in-memory session entry existsSin entrada en memoria; whatsappConfig persistido en DISCONNECTED con phoneJidResponde sessionState persistido, qr: null y phoneJidconnectionRepository
status(): prefers the live in-memory session entry over the persisted fallbackHay entrada en memoria CONNECTED y config persistida DISCONNECTEDResponde CONNECTEDsessionManager.getEntry (spy)

baileys-logger.spec.ts​

createBaileysLogger​

TestEscenarioResultado esperadoMocks
forwards a plain messagewarn('algo pasó')Llama Logger.warn con el mismo mensajeLogger.prototype.warn/error (spies)
keeps the detail object alongside the messagewarn({ trace: 'boom' }, 'failed to upload')Se loguea failed to upload — {"trace":"boom"}Logger spies
unwraps an Error into its stackerror(err, 'upload failed') con un ErrorEl log contiene upload failed — Error: sin conexiónLogger spies
survives a circular object instead of throwingSe pasa un objeto con referencia circularNo lanza y se emite el warnLogger spies
drops info, debug and traceSe llama info, debug y traceNo se emite nada (ni log ni warn)Logger.log (spy)
returns a child that keeps loggingchild({ class: 'baileys' }).warn(...)El hijo sigue logueando el mensajeLogger spies
degrades the expected 515 stream error to debugerror con fullErrorNode stream:error código 515No se emite error; se baja a debugLogger.debug (spy)
keeps a session conflict at error levelerror con reasonNode conflict / replacedSe mantiene en nivel errorLogger spies

incoming-contact.helper.spec.ts​

resolveIncomingContacts​

TestEscenarioResultado esperadoMocks
returns undefined for a message without shared contactsMensaje conversation de textoundefinedNinguno
reads the name and the WhatsApp id of a shared contactcontactMessage con vCard FN y TEL;waid=...[{ name: 'WT pruebas', phone: '593963185056' }]Ninguno
falls back to the vCard full name and telephone when WhatsApp omits themSin displayName; vCard con FN;CHARSET y TEL planoUsa nombre y teléfono de la vCard (573001234567)Ninguno
reads every contact of a contacts array messagecontactsArrayMessage con dos contactosDevuelve ambos contactos en ordenNinguno
skips contacts without a phone and returns undefined when none is usablevCard sin TELundefinedNinguno
uses the phone as name when the contact has noneSin nombre, solo waidname igual al teléfonoNinguno
unwraps a contact shared in a chat with disappearing messagesContacto dentro de ephemeralMessageDesenvuelve y devuelve el contactoNinguno

incoming-location.helper.spec.ts​

resolveIncomingLocation​

TestEscenarioResultado esperadoMocks
returns undefined for a message without a locationMensaje de textoundefinedNinguno
builds a maps url from the coordinateslocationMessage con latitud y longitudURL https://maps.google.com/maps?q=<lat>%2C<lng>&z=17&hlNinguno
treats a live location as a fixed pointliveLocationMessageMisma URL de mapas con las coordenadasNinguno
unwraps a location sent in a chat with disappearing messagesUbicación dentro de ephemeralMessageURL de mapas correctaNinguno
returns undefined when the coordinates are missinglocationMessage solo con nameundefinedNinguno
keeps a zero coordinateLatitud y longitud igual a 0URL con q=0%2C0 (el cero no se descarta)Ninguno

incoming-media.helper.spec.ts​

resolveIncomingMedia​

TestEscenarioResultado esperadoMocks
returns undefined for a plain text messageMensaje conversationundefinedNinguno
maps an image with its captionimageMessage jpeg con captionfileType IMAGE, mimetype, filename: 'imagen.jpg' y captionNinguno
stores a sticker as an imagestickerMessage webpfileType IMAGE y filename: 'sticker.webp'Ninguno
stores a round video (ptvMessage) as a regular videoptvMessage mp4fileType VIDEO y filename: 'video.mp4'Ninguno
stores a regular video toovideoMessage con captionfileType VIDEO y caption conservadoNinguno
unwraps an ephemeral messageImagen dentro de ephemeralMessagefileType IMAGENinguno
unwraps a view-once messageVideo dentro de viewOnceMessageV2fileType VIDEONinguno
unwraps nested containersephemeralMessage que contiene viewOnceMessage con ptvMessagefileType VIDEO (dos capas)Ninguno
names a voice note apart from a regular audioaudioMessage con ptt: true y otro sin pttnota-de-voz.ogg y audio.mp3 respectivamenteNinguno
keeps the original filename of a documentdocumentMessage pdf con fileNamefileType DOCUMENT, filename original y caption vacíoNinguno
accepts a document of an unknown typeDocumento application/zipSe acepta como DOCUMENT conservando el mimetypeNinguno
unwraps documentWithCaptionMessageDocumento envuelto con captionDevuelve filename y caption del documento internoNinguno
returns undefined for a reactionreactionMessageundefinedNinguno

jid.helper.spec.ts​

isDirectContactJid​

TestEscenarioResultado esperadoMocks
accepts a phone-number JID5511999999999@s.whatsapp.nettrueNinguno
accepts a linked-id JID123456789@lidtrueNinguno
rejects a group JID...@g.usfalseNinguno
rejects the status broadcast JIDstatus@broadcastfalseNinguno
rejects a broadcast-list JID1234567890@broadcastfalseNinguno
rejects a newsletter/channel JID...@newsletterfalseNinguno
rejects a bot JID...@botfalseNinguno
rejects an unknown suffixsomeone@communityfalse (lista blanca: un sufijo nuevo queda fuera sin cambiar código)Ninguno
rejects an absent JIDundefined, null y cadena vacíafalse en los tres casosNinguno

resolveContactJid​

TestEscenarioResultado esperadoMocks
prefers the phone JID carried in remoteJidAlt over a lid remoteJidremoteJid es @lid y remoteJidAlt es el JID de teléfonoDevuelve el JID de teléfonoNinguno
keeps remoteJid when it is already the phone JIDremoteJid ya es teléfono y el alt es @lidDevuelve remoteJidNinguno
falls back to remoteJid when there is no altSolo remoteJidDevuelve remoteJidNinguno
leaves a group JID untouchedremoteJid de grupoLo devuelve tal cual (lo descarta la lista blanca, no el resolvedor)Ninguno
returns the lid when no phone JID is available anywhereSolo @lid, sin alternativaDevuelve el @lidNinguno
handles an empty keyObjeto vacío {}undefinedNinguno

media.helper.spec.ts​

buildWhatsappMediaContent​

TestEscenarioResultado esperadoMocks
maps an image to Baileys image contentTipo IMAGE{ image: { url }, mimetype }Ninguno
maps a videoTipo VIDEO{ video: { url }, mimetype: 'video/mp4' }Ninguno
sends the precomputed thumbnail so the video shows a previewVideo con thumbnailEl contenido incluye jpegThumbnail (evita que Baileys invoque ffmpeg)Ninguno
omits the thumbnail key when the video has noneVideo sin thumbnailNo existe la clave jpegThumbnailNinguno
maps audio as a voice noteTipo AUDIO{ audio: { url }, mimetype: 'audio/ogg; codecs=opus', ptt: true }Ninguno
sends the precomputed duration and waveform so Baileys never recomputes themAudio con durationSeconds y waveformIncluye seconds y waveform como Uint8ArrayNinguno
omits both keys when the file has no audio metadataAudio sin metadatosNo incluye seconds ni waveformNinguno
maps a document with its filenameTipo DOCUMENT pdf{ document: { url }, mimetype, fileName }Ninguno
falls back to document for an unrecognised typeTipo desconocido (hologram)Se envía como documento con mimetype y fileNameNinguno
attaches the caption to an imageImagen con captionIncluye captionNinguno
attaches the caption to a document alongside its filenameDocumento con captionIncluye fileName y captionNinguno
drops the caption on audio, which cannot carry oneAudio con captionEl caption se omiteNinguno
omits the caption key entirely when there is no textImagen sin captionNo existe la clave captionNinguno
always hands the URL over, never a bufferVideoEl primer valor del contenido es { url } (Baileys hace el streaming)Ninguno

quoted-message.helper.spec.ts​

resolveQuotedMessageId​

TestEscenarioResultado esperadoMocks
returns the quoted message id of a text replyextendedTextMessage con contextInfo.stanzaIdDevuelve wamid.PARENTNinguno
returns the quoted message id of a media replyimageMessage con contextInfo.stanzaIdDevuelve wamid.PHOTO_PARENTNinguno
unwraps ephemeral containers before looking for the quoteRespuesta dentro de ephemeralMessageDevuelve wamid.EPHEMERAL_PARENTNinguno
returns undefined for a message that does not reply to anythingTexto simple y un mensaje solo reenviado (isForwarded)undefined en ambosNinguno
returns undefined when the message has no contentMensaje sin messageundefinedNinguno

whatsapp-broadcast.wiring.spec.ts​

WhatsappBroadcastWiringService​

TestEscenarioResultado esperadoMocks
pushes a QR only to the room of the registered requesterSe registra el solicitante y el manager emite qrbroadcastWhatsappQr se llama con el id del solicitante y { connectionId, qr }socketBroadcast, connectionRepository, WhatsappSessionManager real
does not push a QR for a connection with no registered requesterSe emite qr sin solicitante registradoNo se hace broadcastsocketBroadcast
broadcasts session status tenant-wide and persists it to the connectionEl manager emite status CONNECTED con phoneJidbroadcastWhatsappSessionStatus al tenant y updateWhatsappConfig con sessionState y phoneJidsocketBroadcast, connectionRepository.updateWhatsappConfig
forgets the requester once the session connects (no stale QR push on a later reconnect)Se registra solicitante, llega status CONNECTED y luego otro qrEl qr posterior no se envía a nadiesocketBroadcast

whatsapp-connection.wiring.spec.ts​

WhatsappConnectionWiringService​

TestEscenarioResultado esperadoMocks
starts a WhatsApp session when ConnectionService emits "created"ConnectionService (EventEmitter) emite createdsessionManager.start(connectionId, tenantId)sessionManager.start (spy), EventEmitter
does not throw when the session fails to start (logged, not propagated)start rechaza con boomEl emit no lanza (el error se registra, no se propaga)sessionManager.start rechazado

whatsapp-inbound.service.spec.ts​

El spec mockea downloadMediaMessage de Baileys y usa un contactLock de paso directo (withContactLock). Los mensajes entrantes se simulan emitiendo inbound / ack en el WhatsappSessionManager.

WhatsappInboundService​

TestEscenarioResultado esperadoMocks
resolves the contact by JID, resolves the ticket, and persists the inbound messageMensaje de texto de un contacto nuevofindOrCreate con externalId y username; resolveContactTicket con el texto; create con externalMessageIdcontactRepository, ticketService, messageRepository, tenantSettingsService
links a WhatsApp reply to the quoted message so the backoffice shows what the contact answeredRespuesta con stanzaId de un mensaje conocidoSe guarda con parentId y se hace broadcast con parent (cuerpo del mensaje citado)messageRepository.findByExternalMessageId/findById, ticketRepository.findByIdAndTenant, socketBroadcast
ingests a reply without parent when the quoted message is unknown to the CRMRespuesta citando wamid.UNKNOWNSe guarda sin parentId y no consulta findByIdmessageRepository
dedupe (task 5.2): the same key.id delivered twice results in exactly one MessageMismo key.id entregado dos vecescreate se llama una sola vezmessageRepository.findByExternalMessageId
skips messages with no jid/key.id and messages that are echoes of our own send (fromMe)Mensaje sin remoteJid y otro fromMe: trueNo se crea contacto ni mensajecontactRepository, messageRepository
ignores group, status and other non-1:1 JIDs without touching contacts or ticketsJIDs de grupo, status@broadcast y newsletterNo se toca contacto, ticket ni mensajescontactRepository, ticketService, messageRepository
identifies the contact by the phone JID from remoteJidAlt, not the lidremoteJid @lid con remoteJidAlt de teléfonofindOrCreate con externalId de teléfono y phone: '5511999999999'contactRepository
still ingests a 1:1 message delivered in the same batch as a group oneLote con un mensaje de grupo y uno 1:1Solo el 1:1 se procesa (1 contacto, 1 mensaje)contactRepository, messageRepository
skips messages carrying no text (reactions, deletions, protocol messages)Reacción y conversation en blancoNo se crea contacto ni mensajecontactRepository, messageRepository
ack map (task 5.4): status 2/3/4 update Message.ack by externalMessageId as sent/delivered/readEvento ack con estados 2, 3 y 4advanceAckByExternalMessageId con sent, delivered y readmessageRepository.advanceAckByExternalMessageId
treats PLAYED (5) as readEstado 5Se traduce a readmessageRepository
broadcasts the ack change so the backoffice repaints the tickadvanceAck devuelve messageId/ticketIdbroadcastMessageAckChanged(ticketId, tenantId, [messageId], 'delivered')messageRepository, socketBroadcast
emits nothing when the transition was refusedadvanceAck devuelve nullNo se emite broadcastmessageRepository, socketBroadcast
ignores ack updates with an unmapped status codeEstado 1 sin mapeoNo se llama advanceAckmessageRepository

WhatsappInboundService — avatar del contacto​

El sock.profilePictureUrl y global.fetch están simulados; el storage guarda la copia propia.

TestEscenarioResultado esperadoMocks
downloads the picture and stores our own copy in the public prefixContacto sin foto sincronizada y URL válidaSube a avatars/<tenant>/<contacto>.jpg y actualiza profilePic, profilePicSourceUrl, profilePicSyncedAtsock.profilePictureUrl, fetch, storageService.uploadPublic, contactRepository.update
marks the attempt even when the contact has no picture or restricted itprofilePictureUrl rechaza (forbidden)No sube nada; actualiza solo profilePicSyncedAtsock.profilePictureUrl rechazado, storageService
does not re-download while the copy is freshprofilePicSyncedAt recienteNo consulta la foto ni subesock.profilePictureUrl, storageService
skips the download when the source URL has not changedTTL vencido (30 días) pero misma URL de origenNo hace fetch ni subefetch, storageService
re-downloads once the TTL passed and the picture changedTTL vencido y URL de origen distintaSe sube la nueva copiasock.profilePictureUrl, storageService
ingests the message even when the avatar sync blows upFallan profilePictureUrl y contactRepository.updateEl mensaje se guarda igualmentesock, contactRepository.update rechazado

WhatsappInboundService — adjuntos entrantes​

TestEscenarioResultado esperadoMocks
downloads an incoming image and persists it as a media messageimageMessage jpegstoreIncoming con mimetype y type: image; mensaje messageType: 'media', body: 'imagen.jpg'downloadMediaMessage, fileService.storeIncoming
persists the caption apart from the filenameImagen con captionbody es el nombre de archivo y caption va separadodownloadMediaMessage, fileService
emits new-file-message with a freshly signed urlImagen entrantebroadcastNewFileMessage con URL firmada nueva y no se usa broadcastNewMessagestorageService.getSignedUrl, socketBroadcast
skips the message when the download failsdownloadMediaMessage rechaza (media expired)No se crea el mensajedownloadMediaMessage rechazado
still ingests a plain text message as chatTexto simplemessageType: 'chat' sin storeIncomingfileService
ingests a view-once message that arrives with no content at allisViewOnce: true y message: {}No descarga; guarda chat con aviso WHATSAPP_VIEW_ONCE_NOTICE y viewOnce: truedownloadMediaMessage, fileService
does not download an attachment that comes flagged as view-onceImagen marcada isViewOnceNo descarga y se guarda sin MediaIddownloadMediaMessage
emits the view-once notice over new_message, not new-file-messageAviso de "ver una vez"broadcastNewMessage con viewOnce: true; no se emite new-file-messagesocketBroadcast
leaves a regular image without the view-once flagImagen normalEl mensaje no incluye viewOnce: truemessageRepository
persists a location as its own message type, with the maps urllocationMessage con coordenadasmessageType: 'location', body con URL de mapas, sin descarga; broadcast con messageType: 'location'messageRepository, socketBroadcast
persists a shared contact card with its name and phonecontactMessage con vCardmessageType: 'contact', body con el nombre y sharedContacts con nombre y teléfono; también en el broadcastmessageRepository, socketBroadcast
does not download a redelivered messagefindByExternalMessageId devuelve un mensaje existenteNo se descarga el adjuntomessageRepository, downloadMediaMessage

whatsapp-outbound.dispatcher.spec.ts​

WhatsappOutboundDispatcher​

TestEscenarioResultado esperadoMocks
registers itself for the whatsapp-qr channel on module initSe ejecuta onModuleInitchannelType es WHATSAPP_QR y el registro lo resuelve para ese canalOutboundDispatcherRegistry real
no-ops when the ticket has no connectionIdTicket sin connectionIdNo consulta contactoscontactRepository
task 5.5: connection not ready (no active socket) — message stays persisted, dispatch does not throw and does not try to sendNo hay socket activodispatch resuelve sin error y no consulta contacto ni asigna externalMessageIdcontactRepository, messageRepository
sends the WhatsApp markup of a text message untouchedCuerpo *Richard Garcia*: holasendMessage(jid, { text }) con el texto sin alterarsock.sendMessage, contactRepository.findByMongoId
sends via the active socket and attaches the resulting externalMessageIdSocket activo y envío correctoEnvía el texto y guarda externalMessageId wamid.OUT1sock.sendMessage, messageRepository.setExternalMessageId

markRead​

TestEscenarioResultado esperadoMocks
acknowledges the contact messages by rebuilt keyDos mensajes con ids externos del contactoreadMessages con claves reconstruidas (remoteJid, id, fromMe: false)sock.readMessages, messageRepository.findExternalMessageIdsByIds, contactRepository
does nothing when none of the messages came from WhatsAppNo hay ids externosNo llama readMessages ni consulta contactomessageRepository, contactRepository
does not throw when the connection is downSin socketResuelve undefined sin consultar mensajesmessageRepository

quoted replies​

TestEscenarioResultado esperadoMocks
quotes the contact message the agent replied to, so the phone shows the replyMensaje con parentId de un mensaje del contactosendMessage con quoted (fromMe: false, id externo y texto del padre)sock.sendMessage, messageRepository.findById
marks the quote as ours when the agent replies to a message sent from the CRMEl padre es sendType: 'user'La clave citada lleva fromMe: truemessageRepository.findById
quotes the caption when the replied message was an attachmentEl padre tiene captionEl mensaje citado usa el caption como conversationmessageRepository.findById
sends without quote when the replied message never reached WhatsAppEl padre no tiene externalMessageIdEnvía solo (jid, { text }), sin tercer argumentomessageRepository.findById
ignores a replied message that belongs to another tenantEl padre pertenece a otro tenantSe envía sin citamessageRepository.findById
does not look up a parent for a message that is not a replyMensaje sin parentIdNo se llama findByIdmessageRepository

attachments​

TestEscenarioResultado esperadoMocks
sends the attachment as media, resolving a fresh signed URLMensaje con MediaId de un PDFgetSignedUrl(storageKey); envía document con mimetype y fileName; guarda externalMessageIdfileRepository.findByIdAndTenant, storageService.getSignedUrl, sock.sendMessage
carries the caption through to the channelImagen con captionEnvía image con mimetype y captionfileRepository, storageService, sock
sends nothing when the referenced file is goneEl archivo ya no existe (null)No se envía nada (evita mostrar un nombre de archivo como texto)fileRepository
still sends a plain text message as textMensaje sin MediaIdNo consulta archivos y envía { text }fileRepository, sock
sends the WhatsApp markup of the caption untouchedCaption con marcado *...*El caption llega sin modificarfileRepository, storageService, sock

whatsapp-session.manager.spec.ts​

Usa un socket falso basado en EventEmitter, timers falsos de Jest y socketFactory/authStateFactory/qrRenderer inyectados.

WhatsappSessionManager​

TestEscenarioResultado esperadoMocks
qr -> connected: emits qr then transitions to connected on connection openSe inicia la sesión, llega qr y luego connection: openEstado INIT, luego QR (emite qr con data URL) y después CONNECTED (emite status)socketFactory, authStateFactory, qrRenderer
wires creds.update to saveCredsEl socket emite creds.updateSe invoca saveCredsauthStateFactory
loggedOut (401) close clears auth state, removes the entry, and does not reconnectCierre con statusCode 401Se ejecuta clear, se elimina la entrada y no se reconecta (1 sola llamada a socketFactory)socketFactory, timers falsos
creates the socket with an explicit logger and history sync disabledstart()La config tiene syncFullHistory: false, markOnlineOnConnect: false y un logger con warnsocketFactory
a transient close (non-401) schedules a bounded reconnect that opens a new socketCierre con código 408Estado DISCONNECTED sin clear; tras BASE_DELAY_MS se crea un segundo socketsocketFactory, timers falsos
start() is idempotent while the entry is non-terminal (no double socket)start() dos veces seguidasDevuelve la misma entrada y crea un solo socket y un solo auth statesocketFactory, authStateFactory
caps reconnect attempts at WHATSAPP_RECONNECT.MAX_ATTEMPTSCierres repetidos (MAX_ATTEMPTS + 2)Total de sockets limitado a 1 + MAX_ATTEMPTSsocketFactory, timers falsos
logout() ends the socket, clears auth state, and marks the entry terminallogout()Llama sock.logout, clear y elimina la entradafakeSocket.logout, clear

onModuleInit() — staggered rehydrate on boot​

TestEscenarioResultado esperadoMocks
queries persisted sessions with non-null creds to decide what to rehydrateonModuleInitConsulta find({ creds: { $ne: null } })sessionModel.find
staggers restarts by WHATSAPP_REHYDRATE_STAGGER_MS instead of rehydrating every candidate at onceTres sesiones persistidasNada se ejecuta de forma síncrona; arrancan una a una separadas por WHATSAPP_REHYDRATE_STAGGER_MSsessionModel.find, start (spy), timers falsos
rehydrates a session with valid persisted auth straight to connected, without requesting a new QRUna sesión con credenciales válidasPasa de INIT a CONNECTED al abrir, sin emitir qrsessionModel.find, socketFactory
a failure rehydrating one session does not stop the others from being attemptedLa primera sesión falla con boomSe intenta la segunda y se registra Rehydrate failed for connection conn-failstart (spy), Logger.warn