This update integrates the enviarTextoFixoPendente function into the followup-flow-worker, ensuring that pending fixed text is drained to prevent messages from getting stuck in a pending state. Additionally, tests have been updated to verify the correct invocation of this function under various scenarios.
O follow-up ficava preso em waiting_reply porque so o cron diario de LGPD estava registrado. vercel.json na raiz agenda a mesma cadencia do scheduler, e o CRON_SECRET nativo deixa de tomar 403.
This update introduces a new function, alinharNomeAoTetoWaha, to ensure that session names conform to WAHA's maximum length requirement. The function generates a new session name if the current one exceeds the limit, preventing HTTP 400 errors during session creation. Additionally, tests have been added to validate the new naming constraints and ensure proper functionality across the application.
Duas pessoas acharam este defeito no mesmo dia, sem uma saber da outra —
@luiscgc91 (#683) e @rafaelbatistazz (issue #715 + #726) —, as duas instalando
numa VPS limpa, e as duas escreveram EXATAMENTE a mesma correção. O CHANGELOG
dizia "um contribuidor de fora"; agora diz quem.
E o texto passou a ser o dele, porque é melhor: nomeia o que a pessoa VÊ —
"para logo depois de «chave de cifra ativa no banco»", "mostra «A instalação
parou»", "com o CRM já no ar", "rodar de novo contornava". É descrição de quem
viveu, não de quem leu o diff — e é isso que faz alguém reconhecer o próprio
problema numa lista de mudanças.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`i18n-espanhol-cobre-a-tela` reprovou as duas `t()` que o aviso novo
acrescentou. O gate está certo e é bom: sem tradução, a tela em espanhol **cai
no português em silêncio** — não quebra, só fala outra língua para quem
escolheu a dela.
Vale notar o que ele NÃO deixa passar: o texto do aviso é longo e explica o
risco da conta. Traduzi inteiro, não resumido — uma versão curta em espanhol
diria menos sobre o risco do que a portuguesa, e o ponto do aviso é justamente
que a pessoa leia antes de aceitar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dois modos novos, os dois pagos na mesma rodada de 11/09.
**50 — o `origin` é do repositório, não do worktree.** Um push falhou com
`repository 'https://github.com/alguem/DeskcommCRM.git/' not found`: o remoto
tinha sido apontado para o endereço de EXEMPLO da documentação. Worktree não tem
config próprio de remoto, então isso quebra `fetch` e `push` de todas as sessões
ao mesmo tempo — e o erro lê como problema de credencial. A sonda vem antes de
mexer em token: `git remote get-url origin`.
**51 — o Next põe um `role="alert"` em toda página.** O anunciador de rota
(`__next-route-announcer__`, vazio) casa qualquer `getByRole("alert")` e reprova
por strict mode COM o alerta certo visível na tela — o que faz o vermelho
parecer defeito de produto. Peça o elemento, não só o papel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`verify` reprovou com `expected 503 to be 403` e `expected 503 to be 422` em
`voz-rotas-de-chamada.test.ts`, e o vermelho apontou um defeito de DESENHO que
eu tinha introduzido, não um ajuste de fixture.
═══ DUAS FONTES PARA O MESMO FATO ═══
A rota resolve `getWacallsClient()` e devolve 503 quando ele é nulo — nesse
ponto a instalação **comprovadamente** oferece voz. A guarda relia
`env.WACALLS_API_BASE_URL` e perguntava de novo, por outro caminho. É assim que
duas respostas divergem: um teste que dubla o cliente e não dubla o env ganha um
503 de "instalação não oferece" numa rota cujo cliente existe.
`exigirVozLigada` ganhou `opts.instalacaoOferece`, e as duas rotas passam `true`
porque já provaram o fato uma linha acima. O default continua sendo o env, para
quem chamar sem saber.
═══ E A PRÉ-CONDIÇÃO QUE O TESTE PASSOU A TER ═══
Com a guarda no lugar, `POST /voice/calls` exige o consentimento da organização
ANTES das regras de contato. Sem semear `org_voice_calls`, os casos de opt-out e
de anonimização mediam a recusa errada — passavam pelo motivo errado, que é pior
que falhar.
O `beforeEach` semeia `enabled: true`, com o motivo escrito, e a recusa ganhou
três casos PRÓPRIOS, que é onde ela deve ser medida:
- organização que **nunca escolheu** (`escolha = null`, o estado de toda
organização antes de alguém aceitar o risco) → 422, e `startCall` não é
chamada, e nada é inserido;
- organização que **desligou** → 422;
- **leitura que não volta** → 503 `voice_estado_indeterminado`, e NÃO
`voice_desligada_na_organizacao`. "Não sei" disfarçado de "está desligada" faz
quem opera procurar um interruptor quando o problema é o banco.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A asserção anterior estava no alvo certo — o alerta existe e diz o que deve:
1) <p role="alert">Não foi possível aceitar este convite. Ele pode t…</p>
2) <div role="alert" aria-live="assertive" id="__next-route-announcer__"></div>
O segundo é do Next, injetado em TODA página e vazio. `getByRole("alert")` casa
os dois e reprova por strict mode.
`p[role="alert"]` escolhe o nosso. O motivo ficou escrito: quem ler
`getByRole("alert")` num spec deste repo precisa saber que o Next põe um
concorrente global em toda rota.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`/app/inbox/<id>` não é a URL final: a tela redireciona para
`/app/inbox?id=<id>` e só DEPOIS monta os painéis. Um `goto` seguido de asserção
começa a medir com a navegação em curso — o painel já existe no DOM, vazio, e o
texto chega alguns instantes depois.
Medido em 11/09/2026, com o CI saturado, em DOIS PRs diferentes e em asserções
DIFERENTES do mesmo arquivo:
:142 getByTestId('inbox-demandas').getByText('Demanda vigente…')
→ "waiting for navigation to finish"
:223 getByTestId('inbox-memoria') toContainText('Histórico encerrado')
→ "3 × locator resolved to <section …>" — existia e estava vazio
Duas asserções distintas quebrando no mesmo ponto do fluxo não são duas
flakinesses: é o mesmo defeito de espera. E é a SEGUNDA vez que esta classe
morde este repositório — a primeira foi o link direto esperando a lista carregar
duas vezes, que destravou a fila inteira em #640.
O conserto NÃO relaxa nada: nenhum timeout foi aumentado, nenhuma asserção
afrouxou. Ele espera o que é determinístico — a URL final — antes de começar a
medir. Os dois `goto` do arquivo passaram por uma função só, com o racional no
cabeçalho dela, para o terceiro não nascer sem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`e2e-parte (1)` reprovou com:
Locator: getByRole('heading', { name: /inválido ou expirado/i })
Error: element(s) not found
E o caso não podia passar. O heading "Convite inválido ou expirado" é do
SERVIDOR (`app/team/accept-invite/[token]/page.tsx:63`) e sai só quando
`verifyInviteToken` devolve `null` — token adulterado ou vencido.
Aqui o token está íntegro e dentro das 24h. **O que o invalidou foi a LINHA**
(`team_invites.revoked_at`), e essa recusa acontece na server action, com a
página já desenhada. Ela chega pelo `<p role="alert">` do `AcceptInviteForm`,
com o texto "Ele pode ter vencido ou seu acesso foi revogado".
Ou seja: o PRODUTO está certo, e a recusa é visível e bem escrita. Errado estava
o alvo da asserção.
Por que isso não apareceu na execução anterior: o caso 13 falhou lá (o e-mail
semeado contém a palavra "Pendente" — conserto no commit anterior) e este foi
PULADO. **A primeira vez que ele rodou de verdade foi a segunda.** É o modo de
falha de sempre: o vermelho de cima esconde o de baixo, e "não apareceu" leu
como "passou".
O caso ganhou o outro lado junto, que é o que ele existe para provar: além do
alerta, a URL continua na tela de aceite e **nenhum vínculo foi criado** em
`user_organizations`. Sem isso, um alerta visível junto com um aceite
bem-sucedido passaria.
Trabalho original de @matheuspedro360 no PR #664.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O passe 12 manda conferir a publicação por HTTP, não pelo verde do robô. Faltava
dizer que a SONDA também erra, e que o erro dela lê como "a release não saiu".
Calibrando uma sonda nova contra a v1.18.1 (que está no ar), dois defeitos
apareceram que de outro jeito só apareceriam sobre a versão nova, quando já não
dá para distinguir sonda quebrada de release quebrada:
- `gh release view --json isLatest` → `Unknown JSON field`; o campo só existe em
`gh release list`, e o erro foi engolido por um `||` virando "não existe".
- `echo "== \`stable\` …"` → `stable: comando não encontrado`. Crase dentro de
aspas DUPLAS executa, mesmo com o heredoc citado: citar o heredoc protege a
escrita do arquivo, não a execução dele.
E a régua que eu errei primeiro: a tag no registro de imagens NÃO tem o prefixo
`v` (`1.19.0`), embora a do git tenha (`v1.19.0`). Pedir `v1.19.0` ao GHCR
devolve 404 para uma imagem que está lá.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Com a guarda de consentimento no lugar, o `POST /voice/sessions/pair` recusa com
uma mensagem boa — "Um administrador pode ligá-la em Configurações › Segurança".
Só que ela chega DEPOIS do clique, e o clique aqui é "Conectar": a pessoa já
está com o celular na mão para escanear o QR quando descobre que faltava outra
tela.
Fazer alguém agir para descobrir que não podia é pior que dizer antes. O cartão
passa a ler a escolha da organização — a mesma rota que a tela de Segurança usa
— e, quando ela é `false`, mostra o motivo e o caminho no lugar do botão.
⚠️ FALHA ABERTA NA INFORMAÇÃO, e a assimetria é deliberada: o aviso só aparece
com `false` EXPLÍCITO. Se a leitura não voltar, `vozLigada` fica `null` e o botão
continua ali — esconder o caminho de quem está com tudo certo por causa de uma
leitura que falhou seria o erro oposto, e mais caro. Quem fecha a AÇÃO é a rota,
que checa de novo no servidor e não confia nesta tela.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
As duas formas que este arquivo caça são de FIAÇÃO DE TELA: o clique não chega a
lugar nenhum. Existe uma terceira em que a tela está inteira e correta — o
clique chama a rota, a rota grava, a tela reflete — e o controle ainda é
decorativo, porque ninguém LÊ o que foi gravado.
Foi o caso do interruptor de chamada de voz: `exigirVozLigada` tinha zero
chamadores, e dava para parear o segundo aparelho sem passar pelo consentimento.
Esta varredura é cega para isso por construção — ela olha JSX, e ali o JSX está
certo. O escopo ficou escrito porque o próprio cabeçalho já ensina a lição:
"guarda que cobre uma forma da classe e não a outra dá a sensação de que a
classe está fechada, que é pior do que não existir".
Nenhuma linha de código mudou.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Terceira vez no mesmo dia, e a terceira DEPOIS de eu ter escrito o aviso que
dizia "o negrito cabe numa linha só". Isso é o argumento inteiro: a regra
anterior era disciplina ("tome cuidado ao quebrar"), e disciplina falhou três
vezes em 11/09, em três fragmentos diferentes.
A nova é mecânica: **parágrafo de fragmento vai numa linha só, por mais longa
que fique** — o Markdown renderiza igual, e não há como executá-la pela metade.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Duas reprovações de `verify` em 11/09, em fragmentos diferentes, pela mesma
causa: quebrar a prosa a ~80 colunas e a quebra cair no meio de um `**`. O gate
reprova porque os asteriscos chegam literais à tela de quem lê o CHANGELOG.
O aviso ficou onde se escreve fragmento, não na lista de modos de falha — é
regra de formato, e quem a lê no momento certo não paga o vermelho.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O modo 48 dizia que `^e2e$` não casa `e2e-parte`. Está certo e é pouco: medido
com `gh pr checks --json name`, dos cinco contextos que a branch protection
exige, só TRÊS existem com aquele nome. `e2e` vira `e2e-parte (1..3)` — com
espaço e parênteses — e `imagens-ok` vira `imagem-do-app-sobe` mais três
`build-and-push (…)`. Um filtro de igualdade exata sobre os cinco pegou três
checks de dezessete e declarou verde um PR com o e2e rodando.
Trocado por uma sonda que não enumera nome nenhum: pergunta pelo ESTADO
(`any fail` / `any pending` / else verde). No primeiro ciclo ela pegou um
`e2e-parte (1)` vermelho que a anterior tinha declarado verde.
Modo 49 novo: o valor do fixture contendo a palavra que a asserção procura.
`convite.pendente.<uuid>@…` fez `getByText("Pendente")` casar a célula do e-mail
E o selo de status — strict mode violation cuja causa mora trinta linhas acima,
no dado semeado.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O modo 47 citava 17 execuções na fila. O número saiu de
`gh run list --limit 20 | select(queued)`, e no MESMO instante a API dizia 41:
o `--limit` corta a lista antes do filtro, então o resultado é no máximo o
limite. Uma sonda que nunca pode passar de 20 lê como "a fila está sob
controle".
É o `feedback_ausencia_afirmada_a_partir_de_lista_truncada` aplicado a contagem
em vez de ausência. Trocado pelo contador de verdade
(`actions/runs?status=queued --jq .total_count`), com o número medido.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rodada de 11/09/2026 — nove PRs entraram, e cada erro abaixo foi cometido nela.
**8-quinquies — o merge de história (`-s ours`).** O PR #628 originou o épico da
voz e teve o código refeito por inteiro antes de entrar. Nem `git merge <head>`
(traria de volta o que a revisão substituiu) nem fechar (o histórico diria que o
trabalho dele não entrou) estavam certos. A seção traz a receita E a medição que
a torna honesta: `trazidos=52 vivos_na_main=49`. Sem esse número, `-s ours` é
carimbo.
**44 — eu commitei a sabotagem, e o corpo do commit afirmava a restauração.** A
pior variante da família: a mensagem descreve o que foi feito no DISCO, o commit
é o que foi PUBLICADO, e dias depois se lê o próprio corpo como evidência de
estado. O sintoma chega disfarçado de diferença de ambiente — passa aqui,
reprova no CI — e a primeira explicação que se escreve é a errada. A sonda
(`git diff HEAD --stat`) vem ANTES de qualquer hipótese de ambiente.
**45 — `git checkout <pr> -- <arquivo>` reverte trabalho mais novo.** Medido ao
extrair duas linhas do #683: `11 insertions(+), 41 deletions(-)`. O `--stat`
depois do checkout é obrigatório.
**46 — PR que mistura P0 e decisão do dono se PARTE.** Segurar tudo deixa um
conserto de instalação atrás de uma questão de gosto; mergear tudo decide a cara
do produto sem o dono. Dois desfechos, e a frase que o contribuidor precisa
ouvir: "não estou recusando; quem decide isto não sou eu".
**47 — o CI é saturável, e quem satura é você.** Seis PRs em vinte minutos = 17
execuções na fila, e a primeira vítima foi o corte de versão, parado atrás dos
checks dos PRs que ele ia publicar. O corte vai ANTES da próxima leva.
**48 — a sonda do monitor casa o nome errado e declara verde.** `^e2e$` não casa
`e2e-parte`. Um monitor anunciou VERDE com duas das três partes pendentes:
sonda que não encontra o job é indistinguível de job que passou.
**49 — o mecanismo silencioso é acusado fácil demais.** O claim do follow-up
levou a culpa porque `claimed: 0` é indistinguível de "nada vencido" — e estava
funcionando o tempo todo; faltava uma rodada do motor. Chame a função direto,
isolada, antes de escrever uma linha de diagnóstico.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`fragmentos-de-release.test.ts` reprova `**` que abre numa linha e fecha na
outra — os asteriscos chegam literais à tela de quem lê o CHANGELOG. Terceira
vez no mesmo dia, e por isso a regra virou mecânica: parágrafo de fragmento vai
numa linha só, por mais longa que fique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`lib/voice/guarda.ts` exporta `exigirVozLigada`, que lê `org_voice_calls.enabled`
e recusa a ação quando a organização não ligou. Ela nasceu ÓRFÃ. Varredura do
repositório inteiro:
$ grep -rn "exigirVozLigada" . --exclude-dir=node_modules
lib/voice/guarda.ts:74:export async function exigirVozLigada(
Uma ocorrência: a própria declaração. Nem teste.
═══ A CONSEQUÊNCIA, QUE NÃO É ESTÉTICA ═══
A tela de Configurações › Segurança pede, com caixa obrigatória, que quem
administra declare "eu li o aviso e aceito o risco de o WhatsApp bloquear esta
conta". Quem fosse direto a Conexões e escaneasse o QR **pareava sem passar por
ela** — e é o pareamento que cria a exposição: a partir dele existe um segundo
aparelho vinculado ao número da empresa.
Desligar sempre foi real: o `PUT` do opt-in chama `despareaVoz` e desconecta de
verdade. O que não existia era a exigência de LIGAR. **Um consentimento que dá
para pular não é consentimento**, e a tela que o pede vira controle decorativo —
exatamente o anti-pattern que este repositório tem um gate para caçar, e que não
alcança este caso porque aqui a tela não mente sobre o efeito dela: ela mente
sobre ser o único caminho.
═══ ONDE A GUARDA ENTRA, E ONDE NÃO ENTRA ═══
Entra em `POST /voice/sessions/pair` (o ato que cria a exposição) e em
`POST /voice/calls` (o que usa o vínculo para discar). O segundo não é
redundante: uma organização que pareou e depois desligou fica, por um instante,
com sessão viva e escolha `false`.
NÃO entra em `DELETE /voice/sessions`, `reject` nem `hangup` — a porta de saída
nunca depende do interruptor, que é a razão já escrita no cabeçalho da rota de
DELETE. Exigir a feature ligada para conseguir desligá-la deixaria o aparelho
vinculado sem caminho de volta se a flag caísse.
═══ O TESTE PRENDE A CLASSE, NÃO A INSTÂNCIA ═══
`voz-consentimento-e-portao-de-verdade.test.ts` varre o AST das rotas: as duas
que criam exposição TÊM de chamar a guarda, as três de saída NÃO podem, e um
quarto caso reprova se `exigirVozLigada` voltar a ficar sem nenhum chamador em
`app/`. Um teste de unidade com Supabase mockado provaria uma rota e não
impediria a próxima de nascer sem — que foi como esta nasceu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dois pontos frágeis da primeira versão, os dois consertados antes de rodar no
CI — e os dois são a mesma classe: prender aparência em vez de contrato.
1. **O painel era achado por `locator("div").filter({hasText: /^Chamada de voz/})
.first()`.** Isso depende de qual `div` da árvore casa primeiro, e um wrapper
a mais no layout mudaria o elemento medido sem mudar nada visível. O `Card`
ganhou `data-testid="painel-voz"` (ele repassa props para o `div`, conferido)
e o spec passou a isolá-lo por ali. Isso também é o que faz a varredura de
qualidade de texto medir SÓ este painel — a tela de Segurança tem outros
cartões, e uma varredura da página inteira acusaria o vizinho.
2. **A navegação era por RÓTULO** (`getByRole("link", {name: /Configurações/})`).
O texto do menu é matéria de produto e pode mudar; o endereço é o contrato.
Um spec que casa rótulo reprova quando alguém renomeia "Configurações", e
isso não é o defeito que ele existe para pegar. Passou a
`a[href="/app/settings"]` → `a[href="/app/settings/security"]`, que prova a
mesma coisa (dá para CHEGAR clicando) sem prender a palavra.
Conferido no catálogo antes de escrever: o grupo é `organizacao`, o hub é
`/app/settings` com rótulo "Configurações", e `/app/settings/security` está
listado como "Segurança". Se qualquer um dos dois sair do catálogo, o
`toBeVisible` reprova com a mensagem certa.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O épico da voz (#697) entrou com a DoD 12 declarada em aberto: nenhuma spec
dirigia a tela. Este arquivo fecha a parte que um runner do CI consegue provar.
═══ O ESTADO QUE ELE MEDE, E POR QUE É ESTE ═══
Nenhuma instalação nasce com a chamada de voz: o serviço `wacalls` vive atrás do
profile `voz` no compose e `WACALLS_API_BASE_URL` nasce vazia. Então este spec
roda no estado em que **100% das instalações começam** — que é o que a doutrina
de QA Visual manda testar ("com os envs opcionais AUSENTES, é onde moram os
piores bugs de primeira impressão").
Não é preciso configurar nada para chegar nele. E se alguém acrescentar
`WACALLS_API_BASE_URL` ao `.env.e2e`, o caso falha ALTO em vez de virar verde
vazio: ele confere o estado com o backend ANTES de afirmar qualquer coisa sobre
a tela, e o `motivo` esperado é `instalacao_nao_oferece`.
═══ O QUE ELE PRENDE ═══
1. **Desligada por padrão.** Chamada de voz vincula um segundo aparelho ao
número por um caminho não-oficial, e o risco é a CONTA, não a chamada.
Nascer ligada seria decidir esse risco pelo dono do negócio.
2. **O risco vem ANTES do controle**, em português de quem não é técnico.
3. **Nada de controle decorativo.** Sem o serviço, a tela NÃO oferece um botão
que falharia — explica que quem cuida do servidor precisa ligá-lo antes.
Duas asserções `toHaveCount(0)` guardam isso pelos dois botões.
4. **A porta existe** (DoD 14): o segundo caso chega ao painel PELA NAVEGAÇÃO,
clicando, como um leigo faria — não por `goto`.
═══ QUALIDADE DE TELA, POR FERRAMENTA E NÃO A OLHO ═══
Vazamento de chave de tradução (`t()` devolve a chave quando o verbete falta, e
ela chega à tela parecendo texto), asterisco de markdown mal fechado, `{{ }}`,
`undefined`, `[object Object]`, e rolagem horizontal medida por
`scrollWidth` vs `clientWidth` — nunca a olho.
ADMIN e não `agent`, e aqui isso não é preferência: o painel só mostra o
controle para `podeEditar` (`role >= admin`), e um `agent` veria a frase de
permissão — o spec mediria outra coisa e passaria. O admin do seed tem TOTP
verified sempre, daí o challenge.
═══ O QUE ELE NÃO PROVA, DECLARADO ═══
Uma ligação real. Depende de parear um número de verdade (QR num celular) e de
alguém do outro lado do telefone — não é reproduzível num runner, e fingir com
mock seria pior que a ausência escrita. Essa medição é à mão, na VPS, e a
bancada já está de pé.
Entrou na `SPECS_PARTE_3`; `e2e-cobertura-completa` volta a 4/4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`mediaUrl.hostname !== "lookaside.fbsbx.com"` é fail-closed e a direção está
certa: a `url` vem da resposta da Graph API, e seguir cegamente uma URL que
chegou de fora é SSRF, mesmo vindo de um endereço autenticado.
O que muda é a largura. `lookaside.fbsbx.com` é o host que a documentação da
Meta cita, e a leitura mais ampla — inclusive implementações de referência —
descreve a mídia saindo também de hosts `*.fbcdn.net`.
**NÃO CONSEGUI MEDIR ISTO.** Não há conta Meta nesta casa, então não vi uma
resposta real da Graph API. E é justamente a incerteza que decide a direção do
erro: um host legítimo recusado faz a mídia **nunca chegar**, com uma mensagem
que parece problema de segurança e manda quem opera investigar o lugar errado.
Um sufixo da Meta a mais não abre superfície nova — continua `https:` e continua
domínio da Meta.
A mensagem de erro passou a dizer QUAL host veio, que é o dado que faltava para
alguém ampliar a lista com evidência em vez de palpite.
Trabalho original de @carlospaivabr no PR #706.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`fragmentos-de-release.test.ts` reprova `**` que não fecha na mesma linha,
porque os asteriscos chegam LITERAIS à tela de quem lê o CHANGELOG. O meu abria
em "linha 2" do corpo.
Segunda vez hoje, no mesmo dia e por hábito de quebrar linha a ~80 colunas: a
quebra caiu no meio do negrito. A regra é do formato, não do gate — e vale
lembrar disto ao escrever fragmento: **o negrito inteiro cabe numa linha, mesmo
que ela passe das 80 colunas.**
Sem o `pnpm install` na árvore nova, o gate devolvia o vermelho ERRADO
(`Cannot find module 'vitest/config'`) e lia como se o fragmento ainda
estivesse quebrado — modo de falha 35 do procedimento de triagem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`e2e-parte (1)` reprovou com strict mode violation:
getByRole('row', {name: /convite.pendente.adfdb4fd@deskcomm.test/i})
.getByText('Pendente') resolved to 2 elements:
1) <td …>convite.pendente.adfdb4fd@deskcomm.test</td>
2) <div …>Pendente</div> ← o selo de status
O endereço semeado é `convite.pendente.<uuid>@deskcomm.test` e **contém a
palavra "pendente"**. O `getByText` é substring e insensível a caixa por
padrão, então ele casa a célula do e-mail E o selo, na mesma linha.
`{ exact: true }` resolve com precisão: o selo diz exatamente "Pendente"; o
endereço, não. O motivo ficou escrito ali, porque a próxima pessoa a ler
`getByText("Pendente")` não tem como adivinhar que o defeito está no FIXTURE.
Rodapé do Playwright naquele job: `1 failed | 142 passed (16.9m)`, e a lista de
falhas nomeia só este caso. O outro `✘` do log (`degradacao-silenciosa:117`) é
um `test.fail()` deliberado — a catraca que documenta uma lacuna conhecida —,
e por isso não entra na conta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O PR #706 muda comportamento visível a quem opera uma VPS — mídia que não
aparecia passa a aparecer — e não trouxe fragmento. Escrito aqui, creditando
@carlospaivabr.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
═══ O QUE EU ACHEI, E NÃO ERA O QUE NÓS DOIS SUSPEITÁVAMOS ═══
O invariante do #657 reprovava com o enrollment parado em `status: active`, e a
hipótese (minha e de @matheuspedro360) era que `fn_claim_due_followup_enrollments`
estivesse falhando — plausível, porque o `engine.ts` engole falha de claim e o
comentário dele diz com todas as letras que `claimed: 0` é indistinguível de
"nada vencido".
Sondei a função direto, num Postgres de verdade:
SONDA-ESTADO {"status":"active","vencido":true,"claimed_until":null,
"appointment_revision":"2","ap_status":"no_show","recibos":"1"}
SONDA-CLAIM-OK {"n":1}
SONDA-TICK {"claimed":1,"advanced":1,"scheduled":0,"failed":0}
SONDA-POS {"status":"active","current_node_id":"end"}
O claim funcionava o tempo todo. **O motor avança UM nó por rodada:** o primeiro
tick tira o enrollment do gatilho e o deixa PARADO no nó `end`; é o segundo que
EXECUTA o `end` e conclui com `exhausted`. O teste tinha um tick só.
Em produção o cron roda a cada minuto e isso é invisível — não é defeito de
produto. O laço do teste agora roda duas rodadas, com o motivo escrito, e o
diagnóstico fica registrado ali para a próxima pessoa não repetir o caminho.
═══ E O BLOQUEADOR QUE ERA NOSSO ═══
Esta é a QUARTA porta para `appointment_recovery_review`, e as outras três já
guardam anonimização: `fn_appointment_recover` recusa contato anonimizado,
`fn_meet_redact_contact` resolve os abertos com `ref_id=null`, e há um bloco de
cura no baseline. Esta nascia sem — e nenhum invariante cobrava a guarda de quem
escreve o `kind` pelo TypeScript.
A consequência é concreta: a cascata de LGPD NÃO cancela `followup_enrollments`.
Um contato anonimizado com régua em curso chega ao fim dela DEPOIS da redação, e
esta porta reabriria um aviso apontando para o compromisso que a anonimização
tinha desligado.
A guarda foi para DENTRO da escrita — `insert ... select` com join em `contacts`
e `not c.is_anonymized` —, e não num `if` antes do insert. Num `if` ela fica a um
refactor de distância de sumir; no `select` quem mudar a consulta tem de apagar
a linha de propósito. O contato vem do COMPROMISSO, que é o vínculo que a
anonimização de fato percorre.
O invariante novo tem DOIS controles positivos, porque "nenhum aviso" é fácil de
obter por acidente: um afirma que o enrollment continua vivo depois da
anonimização (senão a régua teria morrido antes), e outro que ela CHEGOU AO FIM
(senão não haveria o que avisar).
Trabalho original de @matheuspedro360 no PR #657.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>