fix(testes): elimina a flakiness de core.test.ts (espera por tempo real e isolamento de authDir) - #28
Open
Clintonrocha98 wants to merge 3 commits into
Open
fix(testes): elimina a flakiness de core.test.ts (espera por tempo real e isolamento de authDir)#28Clintonrocha98 wants to merge 3 commits into
Clintonrocha98 wants to merge 3 commits into
Conversation
Causa raiz: o helper emit() dos testes esperava o ev.process registrar
usando um número fixo de voltas do event loop (100 setImmediate), como
proxy pra "connect() já teve tempo de terminar". connect() faz I/O real
de disco (useMultiFileAuthState via libuv). Sob carga — a suíte inteira
rodando em paralelo, ~20+ processos disputando disco/CPU — 100 ticks
podem passar em ~2ms de relógio, tempo real insuficiente pra essa I/O
terminar, disparando "ev.process não foi registrado" à toa. Confirmado
instrumentando connect() e o loop de espera: o timeout de 100
ticks/2ms bateu com useMultiFileAuthState ainda em voo.
O mesmo authDir relativo ('baileys_auth_info') era reusado por todos os
testes do arquivo (cwd fixo uma vez no topo do arquivo), então um
connect() que sobrevive ao teste que o originou — caso comum, já que
stop() nunca esperava o connect()/reconexão em voo — ficava brigando
pelo mesmo diretório/mutex de arquivo do teste seguinte, um ruído de
I/O concorrente a mais alimentando a flakiness.
Testes: troca a espera por ticks por um waitUntil() com orçamento de
tempo real, e dá a cada teste sua própria subpasta de authDir.
src/collector/core.ts: stop() agora espera de fato o connect() em voo
(inicial ou reconexão) terminar antes de fechar outbox/socket, e
connect() aborta cedo se stopped virou true enquanto ele ainda esperava
auth state/versão. Sem isso um connect() abandonado continuava abrindo
socket e registrando handlers depois do stop() "concluído" — em
produção isso significa reconectar ao WhatsApp mesmo com o coletor
supostamente parado.
O connect() aguardado pelo stop() contém uma chamada HTTP (fetchLatestBaileysVersion). Numa rede ruim ela fica pendurada, e o stop() ficava preso atrás dela — justamente antes de fechar o outbox. O efeito prático seria um restart durante reconexão cair no forceMs do shutdown (saída com código 1) sem nunca dar checkpoint no WAL do SQLite: pior que o comportamento anterior, em que o outbox fechava na hora. Com o teto de 2s, o stop() mantém a garantia de quiescência quando o connect() está apenas terminando (o caso normal, e o dos testes: resolve em milissegundos) e desiste de esperar quando ele está travado em rede.
gvieira18
previously approved these changes
Aug 19, 2026
Conflito em tests/collector/core.test.ts: a branch trocava o authDir de todos os testes por nextAuthDir() (isolamento de I/O), incluindo dois casos que a main apagou no refactor "remoção é ato, não estado" — a asserção do enforcer que removia de novo na reentrada e o teste inteiro do /unban. Resolvido pelo lado da main: o comportamento novo vale (reentrada não dispara remoção; não há denylist nem /unban). O que a branch contribui na região — nextAuthDir() no teste que sobrou — o merge já tinha preservado fora do conflito. 269 testes passando; 5 rodadas da suíte e 6 instâncias paralelas de core.test.ts sem nenhuma falha.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contexto
tests/collector/core.test.tsfalhava de forma intermitente, em testes diferentes a cada rodada:loggedOut limpa o authDir e reconecta,webhook != null liga o coletor,repassa cada evento p/ saveEvent,connection.update: connecting e qr propagam,group-participants.update invalida o diretório, entre outros. Medido antes do conserto: 2 de 5 rodadas da suíte completa falhavam.Rodando só esse arquivo, 15/15 passavam — o que explica por que o problema parecia aleatório.
Causa raiz
O helper
emit()esperava oev.processdo socket falso ser registrado contando 100 voltas doevent loop, como se isso fosse proxy de tempo:
Mas
connect()faz I/O de disco real (useMultiFileAuthState: stat/mkdir/readFile via libuv). Com asuíte inteira rodando (~24 arquivos em processos concorrentes disputando disco e CPU), 100
setImmediatepassam em ~2ms de relógio — tempo insuficiente para a I/O terminar. Evidênciacapturada com instrumentação temporária:
Falha por relógio, sem nenhum bug de lógica.
UV_THREADPOOL_SIZE=64não resolveu, descartando"threadpool saturado" como causa isolada.
Agravante: todos os testes usavam o mesmo
authDirrelativo ('baileys_auth_info', com o cwdfixado uma vez no topo do arquivo). Como
stop()não esperava oconnect()em voo, uma conexãosobrevivia ao teste que a originou e ia disputar o mesmo diretório — e o mutex de arquivo do Baileys,
que é module-level por caminho — com o teste seguinte.
O que muda
tests/collector/core.test.tswaitUntil(cond, timeoutMs = 5000): espera por tempo real (Date.now()), não por ticks.nextAuthDir(): cada teste ganha a própria subpasta (auth-N/baileys_auth_info).src/collector/core.ts(código de produção, então justificando)connectPromiseaponta para oconnect()em voo mais recente (inicial ou reconexão viaclose).if (stopped) returnemconnect(), após os awaits de auth state e versão, antes de criar osocket e registrar
ev.process.stop()aguarda esseconnect(), com teto de 2s.O motivo de mexer em produção: sem isso,
stop()retornava enquanto umconnect()(ou umareconexão pós-
close) seguia por trás criandoauthDir, abrindo socket e registrando handlersdepois que o chamador já considerava o coletor parado. Não é só ruído de teste — um
stop()duranteciclo de reconexão não garantia que o coletor parasse de tentar falar com o WhatsApp.
O teto de 2s existe porque
connect()contém uma chamada HTTP (fetchLatestBaileysVersion).Numa rede ruim ela fica pendurada, e um
stop()preso atrás dela nunca chegaria aooutbox.close()— o checkpoint do WAL do SQLite —, caindo noforceMsdoshutdown.ts(saída comcódigo 1). Seria pior que o comportamento anterior, em que o outbox fechava imediatamente. Com o
teto, a garantia de quiescência vale no caso normal (o
connect()só terminando de resolver, que éo caso dos testes) e o shutdown não fica hostage de rede.
Verificação
stop(): 10/10 passaram (246 testes).tsc --noEmit -p tsconfig.test.json: limpo.ev.process não foi registrado.Limitação conhecida
O teto de 2s no
stop()não tem teste:fetchLatestBaileysVersioné importado direto no módulo enão é injetável, então simular a chamada pendurada exigiria um refactor de injeção fora do escopo
deste conserto. O caminho normal (connect resolvendo antes do teto) está coberto pelos 15 testes que
chamam
startCollectorCoreestop().O
waitUntiltem timeout finito (5s): numa máquina absurdamente sobrecarregada a falha ainda seriateoricamente possível — mas por tempo real esgotado, não por um número arbitrário de ticks.