Skip to content

fix(i18n): traduz toasts e completa chaves ES do menu #1474

fix(i18n): traduz toasts e completa chaves ES do menu

fix(i18n): traduz toasts e completa chaves ES do menu #1474

Workflow file for this run

name: e2e
# Issue #63: as specs de tests/e2e/ não rodavam em lugar nenhum. Este job fecha o
# buraco com o subconjunto que não depende de serviço externo.
#
# NÃO conte aqui quantas são: o summary do job CONTA em vez de afirmar, e
# `tests/unit/e2e-cobertura-completa.test.ts` reprova o build quando uma spec do
# disco não está em nenhuma das três listas abaixo. Um número escrito nesta linha
# é a quarta fonte da mesma verdade, e as três anteriores já divergiram — este
# cabeçalho dizia "29 de 33" quando o disco tinha 45 e o job rodava 44.
#
# A execução é dividida em DUAS invocações do playwright de propósito: o
# limitador de login do produto é por IP (60/300s) e no CI todos os specs vêm do
# mesmo 127.0.0.1. Ver o comentário do passo "E2E — parte 1 de 3".
#
# É CHECK OBRIGATÓRIO. A régua, para reconferir em vez de acreditar nesta linha:
# $ gh api repos/melgarafael/DeskcommCRM/branches/main/protection \
# --jq '.required_status_checks.contexts|join(", ")'
# verify, build-and-size, invariants, e2e, imagens-ok # medido em 2026-08-14
#
# São CINCO. A versão anterior deste bloco colava a saída de 2026-08-12, com
# quatro — `imagens-ok` virou obrigatório em 2026-08-13, um dia depois. Trocar
# uma afirmação vencida por outra vencida é o mesmo defeito, e o commit
# bcd88c30 desta branch existe justamente por causa dele: quem lê aqui para
# saber contra o que medir concluiria que imagem quebrada não segura merge.
# Este cabeçalho afirmou o contrário ("NÃO-BLOQUEANTE por ausência… quando N
# execuções seguidas passarem limpas, adicionar aos obrigatórios") por tempo
# demais depois de a promoção ter acontecido — e quem lesse isso mediria o custo
# de uma mudança contra a régua errada.
#
# O job falha honestamente: a primeira versão usava `continue-on-error: true` no
# job inteiro e reportou VERDE com o `supabase start` quebrado e zero teste
# executado. Verde que não distingue "passou" de "nem rodou" é pior que vermelho.
on:
pull_request:
branches: [main]
push:
branches: [main]
# O GITHUB_TOKEN deste job serve a `actions/checkout` (clone) e a
# `actions/setup-node` (manifest de `actions/node-versions`, repo público).
#
# `supabase/setup-cli@v3` merece a frase inteira, porque a resposta curta está
# errada: ele é `using: composite` e aninha `oven-sh/setup-bun`, cujo input
# `token` tem default `${{ github.token }}` — o token É repassado. Com o
# `.bun-version` do setup-cli fixo em semver exato, o caminho que gasta o token
# (api.github.com/repos/oven-sh/bun/git/refs/tags) não é tomado; se um dia for, é
# leitura de repo público e `contents: read` basta. Grep no `action.yml` de topo
# não enumera consumidor de token quando a action é composite: o consumidor mora
# um nível abaixo.
#
# `actions/upload-artifact@v7` não tem input de token: publica pela Actions
# Results API, com ACTIONS_RUNTIME_TOKEN + ACTIONS_RESULTS_URL. Nenhum passo
# `run:` enxerga o GITHUB_TOKEN — ele só entra no ambiente via
# `secrets.GITHUB_TOKEN`, ausente aqui.
permissions:
contents: read
jobs:
# AS DUAS PARTES RODAM EM PARALELO, e o motivo é medido, não estético.
#
# Elas rodavam como dois passos do MESMO job, então somavam contra um único
# teto de 30 min. Medido no run 33125422531, o último verde antes deste
# conserto: parte 2 = 16,3 min, parte 1 = 8,2 min, setup (build + Supabase +
# browser) ≈ 4,5 min — total ≈ 29 min, com 1 min de folga. A folga acabou:
# dois runs da própria `main` foram CANCELADOS em 30 min exatos (23:30→00:00
# e 23:26→23:56 de 2026-08-27), e um PR de UM arquivo foi cancelado junto.
#
# Em paralelo, cada parte paga o setup uma vez e tem o teto inteiro para si:
# o job mais longo passou a ser ≈ 21 min em vez de ≈ 29.
#
# ⚠️ ESSA ÚLTIMA FRASE VENCEU. Ela estava certa quando escrita e descreve o
# efeito do paralelismo, não o estado: dez dias depois o job mais longo era 26
# min e chegou a ser cancelado aos 30. Quem lê "≈ 21 min" para saber se cabe
# mais spec mede contra a régua errada. O estado de hoje está no bloco
# "REBALANCEAMENTO DE 2026-09-04" mais abaixo — que também traz o comando que
# produz o número, porque número escrito envelhece e comando não.
#
# E há um ganho que não era o objetivo: a razão de existirem partes separadas é
# "processo novo, contador de login zerado" — o teto por IP. Runners de
# matrix são máquinas diferentes, com IPs diferentes, então a separação
# deixa de depender de reiniciar o processo.
#
# O teto de 30 min FICA. Ele é o que denuncia a suíte crescendo de novo;
# subi-lo seria trocar um vermelho honesto por um CI que demora mais a cada
# mês sem ninguém perceber.
e2e-parte:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
# Sem fail-fast: se a parte 1 quebrar, ainda quero saber se a 2 também.
# Com ele ligado, o primeiro vermelho esconde o outro e o conserto vira
# uma rodada por parte.
fail-fast: false
matrix:
parte: [1, 2, 3]
# ─────────────────────────────────────────────────────────────────────────
# A COBERTURA DEIXA DE SER PROSA DIGITADA À MÃO.
#
# Antes, as listas viviam nos dois `run:` e a declaração do que NÃO é coberto
# era um `echo` escrito à mão. As duas fontes divergiram em silêncio: o texto
# dizia "32 de 33 specs" quando o disco tinha 39 e o job rodava 36 — e as
# ausentes eram justamente as novas, incluindo `agente-papeis-operador`, a
# prova de tela do épico dos três papéis, que nunca rodou em job nenhum.
#
# Cobertura parcial silenciosa se lê como cobertura total. Agora há UMA fonte
# por lista, o summary CONTA em vez de afirmar, e
# `tests/unit/e2e-cobertura-completa.test.ts` reprova o build quando um arquivo
# de spec não está em nenhuma das três listas.
#
# FORA_DO_CI exige motivo escrito no próprio nome do bloco abaixo — spec fora
# do gate sem razão declarada é a dívida voltando pela porta de serviço.
# ─────────────────────────────────────────────────────────────────────────
#
# ═══ `cadastro-sem-confirmacao-de-email`: A PRECONDIÇÃO É DO PROVEDOR ═══
#
# Ela mede o cadastro com "Confirm email" DESLIGADO no provedor de auth — o
# estado em que `signUp()` já devolve sessão e não existe link nenhum para
# clicar. Isso não é dado que a spec possa semear: é `GOTRUE_MAILER_AUTOCONFIRM`,
# fixado quando o Supabase local sobe, e o `supabase/config.toml` do repo
# declara `enable_confirmations = true` — que é o que `signup-journey` e
# `invite-lifecycle` exercitam. Ligar o autoconfirm por causa desta spec
# trocaria o provedor da SUÍTE INTEIRA, e as duas de cima passariam a medir
# outro fluxo sem que ninguém percebesse.
#
# O gate de todo dia é o par de testes de unidade, que roda no `verify`:
# `app/actions/auth/signUp.test.ts` (a action DIZ que a sessão veio aberta) e
# `tests/unit/cadastro-sem-confirmacao-nao-manda-esperar-email.test.tsx` (a
# tela AGE sobre isso). A spec é a prova pela tela de que os dois, juntos,
# resolvem a jornada — e ela CONFERE a própria precondição em `beforeAll`,
# falhando alto em vez de se auto-pular.
#
# ═══ O 429 DE `olhar-telas-do-epico`: RESOLVIDO DUAS VEZES, EM CAMADAS DIFERENTES ═══
#
# A spec reprovava com **429** em TODAS as 7 telas, falha PRÉ-EXISTENTE NA MAIN (run
# 31343408488, main, 2026-08-10T00:03, lista byte-idêntica).
#
# Duas sessões chegaram à MESMA causa raiz em paralelo, sem se ver, e os dois
# consertos são complementares — nenhum torna o outro dispensável:
#
# - `a60cae2d` (main, PR #220) desligou a telemetria NO AMBIENTE DO CI:
# `SENTRY_DSN: "off"` no build e em todas as partes (e no `perf.yml`). Ver o
# comentário do passo "Build de produção" abaixo — a armadilha é que vazio NÃO
# desliga, seleciona o Sentry da comunidade.
# - `77b4a486` (este PR) consertou O PRODUTO: o SDK do browser mandava duas SESSÕES
# de release health por NAVEGAÇÃO para o DSN da comunidade — de toda instalação
# self-host, não só do CI. `browserSessionIntegration` é default do SDK e
# `integrations: [x]` SOMA aos defaults, então a política "no Sentry da comunidade,
# só erro" estava declarada e não estava em vigor. Detalhe em `lib/sentry/dsn.ts`;
# o gate é `tests/unit/sentry-comunidade-so-erro.test.ts`.
#
# O ingest respondia `429` com `x-sentry-rate-limits: 60::organization:suspended` —
# lista de categorias vazia, isto é, TODAS: a organização estava suspensa por cota, e
# cada tentativa barrada virava erro de console, que é o que esta spec cobra.
#
# FONTE DA VERDADE do ambiente da suíte é `scripts/gerar-env-e2e.sh` (o `.env.e2e`
# que o passo "Publicar" joga no `$GITHUB_ENV`, e que vale também para quem roda
# local). As três linhas `SENTRY_DSN: "off"` de nível de passo FICAM como segunda
# camada — `env:` de passo vence o `$GITHUB_ENV`, então elas protegem o CI se o
# gerador mudar. Duas fontes que CONCORDAM e uma delas declarada como principal;
# apagar as do colega para "limpar duplicação" seria trocar defesa por estética.
#
# Três hipóteses minhas caíram por medição, e o custo delas foi maior que o do
# conserto. As duas primeiras foram descartadas na época:
#
# 1. "a parte 2 está sobrecarregada" — caiu de 69 para 53 testes, falha byte-idêntica.
# 2. "as specs anteriores exaurem o contador" — posta em PRIMEIRO lugar, contador
# limpo, as mesmas 7 telas deram 429, inclusive a primeira.
# 3. "é o fallback em memória do limitador" — foi o que este comentário afirmou como
# "a pista que sobra". MEDIDO no job real: 120 quedas para memória, 103 delas no
# bucket `auth:login:ip` contra teto de 1000. O limitador não barrou nada.
#
# A lição operacional é sobre INSTRUMENTO, não sobre limitador: a mensagem do browser
# para requisição barrada é "Failed to load resource: ... 429" e não diz QUEM
# respondeu; a spec descartava `m.location()` e o relatório não guarda trace, então a
# única cópia do endereço morria ali. Três runs foram gastos adivinhando o dono de um
# 429 cuja URL o próprio teste tinha em mãos. A spec agora registra a URL.
#
# `AUTH_RATE_LIMIT_LOGIN_IP` segue existindo pelo motivo dele (o teto por IP no CI é
# um runner só) e não teve nada a ver com isto. Afrouxar o limitador do produto para
# acomodar teste segue sendo a troca errada — e aqui não foi preciso.
#
# A ordem das listas voltou ao que era: mudá-la foi tentativa que não se sustentou,
# e deixar a mudança de pé com um comentário dizendo "hipótese" seria pior — o
# próximo leitor herdaria uma decisão tomada por um motivo medido FALSO.
#
# O rebalanceamento das duas partes FICA, por outro motivo e sem alegar mais do que
# faz: 6,5 + 8,1 min em vez de 2,7 + 12,2.
#
# ═══ REBALANCEAMENTO DE 2026-09-04 — O PRIMEIRO MEDIDO POR ARQUIVO ═══
#
# A folga acabou de novo. Run 33820766602 (verde), confirmado em seis rodadas:
#
# parte 1: 103 testes · 9,1 min de Playwright · job 18 min
# parte 2: 127 testes · 18,0 min de Playwright · job 26 min
# setup (build + Supabase + browser) ≈ 8-9 min, igual nos dois
# as seis: parte 1 = 18 17 15 15 18 14 min · parte 2 = 26 28 26 26 25 25 min
#
# E não é mais risco: a parte 2 já ESTOUROU o teto — 30 min 17 s num PR, e de
# novo no run 33823969191 (deste PR), CANCELADA aos 30 min com 117 dos 133
# casos terminados.
#
# ── Por que contagem de TESTES não serve como régua ─────────────────────
#
# Este arquivo já registra dois proxies que inverteram o sinal da medição
# (ver o bloco de `SPECS_PARTE_1` abaixo). O terceiro proxy tentador é o
# número de testes por parte, e ele cai na PRÓPRIA tabela acima:
#
# 103 testes / 9,1 min = 5,3 s por teste na parte 1
# 127 testes / 18,0 min = 8,5 s por teste na parte 2
#
# O mesmo "teste" custa 60% mais de um lado. E dentro de uma parte a
# dispersão é MAIOR: medido por arquivo no run 33823969191,
# `agenda-kit-visual` tem 20 casos e custa 21,6 s; `navegacao` tem 9 casos e
# custa 239,2 s. Mover o arquivo com MAIS casos desloca **11× menos tempo**
# que mover o de menos.
#
# ── O instrumento, para não haver quarto proxy ──────────────────────────
#
# O passo de execução roda com `--reporter=list` (o `dot`, default em CI,
# despeja os pontos em rajada — 37 numa linha só, cobrindo 4,9 min). Cada
# caso vira uma linha carimbada pelo Actions, e o custo de RELÓGIO de um
# arquivo é o intervalo entre o último caso dele e o último caso do arquivo
# anterior — o que inclui o `beforeAll`, que é onde as specs pagam o
# `execFileSync` dos seeds.
#
# Somar a duração que o reporter imprime NÃO serve: `result.duration` é só o
# caso. Medido na parte 1 do 33823969191 — soma das durações 7,14 min contra
# 7,95 min de relógio contra 8,1 min do rodapé do Playwright. O relógio
# fecha em 98% do rodapé; a soma das durações joga fora 12%, concentrado
# justamente nas specs que semeiam.
#
# Para refazer a medição em qualquer run, em vez de acreditar nos números
# desta seção:
#
# gh run view --job <id-do-job> --log > /tmp/e2e.log
# grep -E '\[chromium\] › tests/e2e/' /tmp/e2e.log
#
# ── O que mudou, e por que UM arquivo só ────────────────────────────────
#
# `navegacao.spec.ts` vai da PARTE_2 para a PARTE_1. Medido: 239,2 s
# (3,99 min), 23,3% de todo o relógio que a parte 2 chegou a gastar antes do
# corte. É o maior item das duas partes depois de `prova-painel-provedores`,
# e move sozinho quase exatamente o que falta.
#
# Um arquivo só, e não três de peso médio, porque as duas partes rodam
# contra o MESMO banco sem reset: cada spec movida é uma vizinhança nova.
# `navegacao` é a que tem menos aresta — só afirma cromo de navegação
# (sidebar, abas, ⌘K, medidas de layout), não lê dado semeado por vizinha
# nenhuma, e a precondição dela (`afirmarAdminDeTenantPuro`) é satisfeita em
# qualquer parte, porque quem promove `platform_admin` é
# `seed-e2e-system-update` e ele promove o `e2e-dono` REVOGANDO o `e2e-admin`.
# Alfabeticamente cai entre `mfa-opcional` e `notificacoes-`, isto é, ANTES
# de `relogio-http-cron-externo` — a restrição do tick global segue de pé.
#
# ── PREVISÃO (é previsão; a rodada seguinte deste PR é que mede) ────────
#
# Aplicada ao run de referência (9,1 + 18,0 = 27,1 min), com os 8 arquivos
# que o corte impediu de medir estimados em ~2,4 min (16 casos à média de
# 8,8 s/caso da própria parte 2), `navegacao` vale ≈ 3,6 min lá:
#
# parte 1 ≈ 12,7 min de Playwright → job ≈ 21,6 min
# parte 2 ≈ 14,4 min de Playwright → job ≈ 22,4 min
# folga contra o teto: ≈ 7,6 min no pior dos dois (era 4 min, e virou −0:17)
#
# ── A próxima alavanca, quando esta acabar ──────────────────────────────
#
# `prova-painel-provedores` é 199,4 s — 42% de toda a parte 1, num arquivo
# de 8 casos a 25 s cada. Se a parte 1 voltar a estourar, é lá que se olha
# antes de repartir o resto. E se as duas estourarem juntas, a alavanca
# deixa de ser a partição e passa a ser uma terceira parte: o setup de
# 8-9 min é pago por parte, então três partes custam ~9 min de runner a mais
# e devolvem ~9 min de relógio a cada uma.
env:
# Provider controlado; preview/assistência executam core, ferramentas e gates reais.
INTERNAL_AGENT_RUN_STUB: "true"
SPECS_PARTE_1: >-
pre-go-live-whatsapp.spec.ts
smoke.spec.ts auth.spec.ts error-pages.spec.ts
icone-da-marca.spec.ts
password-recovery.spec.ts signup-journey.spec.ts
primeiro-acesso-sem-organizacao.spec.ts
rbac-roles.spec.ts inbox-scope.spec.ts reset-password-mfa.spec.ts
degradacao-silenciosa.spec.ts vps-webhook-outbound-ssrf.spec.ts
kanban-owner-filter.spec.ts queue-assign.spec.ts
conversa-vira-lead.spec.ts contato-salva-email.spec.ts
confirmar-dado-do-contato.spec.ts
credenciais-de-ia.spec.ts
risk-radar.spec.ts invite-lifecycle.spec.ts system-update.spec.ts
agente-papeis-operador.spec.ts prova-painel-provedores.spec.ts
followup-linguagem.spec.ts followup-ramos.spec.ts
wizard-do-funcionario.spec.ts mfa-opcional.spec.ts inbox-responder-citando.spec.ts
agenda-tela-do-produto.spec.ts
notificacoes-diz-o-que-falta.spec.ts
relogio-http-cron-externo.spec.ts
j20-elegibilidade-respondi.spec.ts j20-elegibilidade-followup.spec.ts
j20-elegibilidade-atendimento-manual.spec.ts
navegacao.spec.ts
juntar-contatos-duplicados.spec.ts
lote-no-quadro-do-funil.spec.ts
importar-leads-planilha.spec.ts
relatorio-de-atividades.spec.ts
# ⚠️ A ORDEM DESTA LISTA NÃO DECIDE A ORDEM DE EXECUÇÃO. O Playwright
# ordena os arquivos por CAMINHO, não pela ordem em que são passados na
# linha de comando. Medido no run 31838253496: `marca-logo.spec.ts` estava
# escrita por último aqui e executou em 15º de 23 — a linha de progresso
# `···F°°··············` mostra 14 testes completando DEPOIS dela.
#
# Este comentário afirmava, por três commits, que ela era "a ÚLTIMA de
# propósito" e que isso limitava contaminação a zero specs. Era falso, e
# a consequência foi pior que o comentário errado: creditei a esse
# reposicionamento o desaparecimento do React #418 entre dois ciclos.
# Não foi ele — a ordem nunca mudou. Foi o `test.afterAll` da própria
# spec, que passou a limpar as DUAS camadas de marca e por isso as specs
# seguintes deixaram de ver logo gravado.
#
# O que continua verdade, e a frase precisou ser corrigida em 2026-09-07
# porque descrevia o mundo de ANTES da matrix: as specs de UMA MESMA parte
# rodam contra o MESMO banco, sem reset entre elas — o que uma spec deixa
# gravado, as seguintes daquela parte enxergam. Quem protege contra isso é
# a limpeza da spec, não a posição dela nesta lista.
#
# Entre partes DIFERENTES não há banco compartilhado: cada job da matrix é
# um runner próprio que paga o seu setup inteiro (~8,5 min, medidos iguais
# em cada um). É por isso que mover spec entre partes é risco: ela troca de
# vizinhança, e a vizinhança é que deixou o banco no estado que ela espera.
#
# ⚠️ Ela esteve no TOPO desta lista por três commits, pela razão errada.
# Eu media "carga de login por parte" com um regex que contava a PALAVRA
# login — e casava comentário, nome de helper e string. O proxy não só era
# impreciso: ele INVERTEU o sinal (dizia 162 vs 145; o real, contando
# chamadas, é 63 vs 82), então mover a spec "para a parte mais leve" a
# mudou para a mais carregada. E o critério era irrelevante desde o
# começo — o `AUTH_RATE_LIMIT_LOGIN_IP: "1000"` mais abaixo desliga o
# teto no CI, e esta spec faz 6 logins (eram 3 até a revisão que descobriu
# que dois casos rodavam DESLOGADOS: `page` é fixture de escopo de teste,
# não de worker — cada caso precisa do seu login).
#
# E o desfecho: a movida não teve efeito NENHUM, porque a ordem é por
# caminho (ver o bloco acima). Duas medições erradas em sequência levaram
# a uma mudança inócua — que eu ainda por cima creditei por um resultado
# que veio de outra coisa. Fica escrito porque o erro instrutivo não é o
# proxy ruim: é ter medido de novo, com outro instrumento ruim, em vez de
# perguntar se o mecanismo suposto existia.
#
# (Este comentário mora FORA do bloco `>-` porque dentro dele `#` não é
# comentário: é conteúdo, e cada palavra viraria argumento do playwright.
# Medido: com o texto dentro, a variável ia de 23 para 205 tokens.)
#
# `agente-marca-consulta` entrou aqui em 2026-08-27, e a dívida que a segurava era
# explícita: ela estava em FORA_DO_CI com o motivo escrito de que eu não conseguira
# EXECUTÁ-LA. Foi executada em ambiente completo (banco do baseline, app buildado),
# 3 casos / 3 verdes, e o primeiro run achou um defeito real de servidor — a tela não
# dizia de quem era o compromisso. Entra na PARTE_2 e não na 1 porque é AQUI que mora
# o molde dela, `agente-organiza-operacao`; o comentário antigo dizia PARTE_1 e estava
# errado sobre o próprio vizinho.
# `relogio-http-cron-externo` entrou na PARTE_1 em 2026-08-28, e a escolha da
# parte é o cuidado que ela exige: o tick que ela dispara é GLOBAL por
# natureza — ele roda as tarefas de minuto da instalação inteira, não só as
# da fixture dela. Um enrollment de outra spec que esteja vencido naquele
# instante É processado.
#
# Por que isso não quebra ninguém, medido e não presumido: as specs que
# criam enrollment e depois conferem `current_node_id` são
# `followup-journey`, `followup-queue` e `followup-dossie` (PARTE_2, outro
# processo, depois) e `followup-ramos` (PARTE_1). A ordem de execução é por
# CAMINHO — ver o bloco acima —, e `followup-ramos` < `relogio-http-...`,
# então ela já terminou quando o tick roda. Nenhuma spec posterior da
# PARTE_1 depende de enrollment parado.
#
# Se um dia uma spec de follow-up entrar na PARTE_1 com nome depois de
# `relogio-`, é aqui que se olha: ou ela vai para a PARTE_2, ou esta vai.
SPECS_PARTE_2: >-
agente-marca-consulta.spec.ts
agenda-marcar-pela-tela.spec.ts
agenda-remarcar-e-cancelar.spec.ts
agenda-tipos-de-agendamento.spec.ts
agenda-grade-interativa.spec.ts
agente-novo-e-uso.spec.ts
agente-organiza-operacao.spec.ts
central-de-avisos-capacidades.spec.ts
escalacao-ciclo.spec.ts
followup-builder.spec.ts
followup-queue.spec.ts
distribuicao-atendimento.spec.ts
followup-journey.spec.ts
capacidades-do-agente.spec.ts
escopo-de-funil-do-agente.spec.ts
followup-dossie.spec.ts
followup-tempo-adaptativo.spec.ts
gatilho-de-etapa.spec.ts
gatilho-de-caso.spec.ts
agenda-ocupacao-do-google-na-grade.spec.ts
agenda-ocupacao-do-google-no-historico.spec.ts
followup-publicado-abre-no-construtor.spec.ts
automacao-diz-a-verdade.spec.ts
acervo-de-conhecimento.spec.ts
agenda-kit-visual.spec.ts
agenda-conectar-google.spec.ts
agenda-painel-cabe-na-tela.spec.ts
agenda-ver-na-agenda.spec.ts
agenda-escopo-da-organizacao.spec.ts
agenda-caminho-ate-os-horarios.spec.ts
agenda-google-volta-do-consentimento.spec.ts
agenda-google-volta-nao-desloga.spec.ts
# `moeda-da-organizacao` entrou na PARTE_2 em 2026-09-04, ao lado de
# `navegacao` — mesmo domínio (Configurações) e a mesma ausência de
# dependência: sem WAHA, Resend, Nuvemshop ou Redis. Toca a organização
# única do banco do CI (troca a moeda e devolve para BRL no afterEach),
# o mesmo cuidado que `troca-de-organizacao-tem-volta` já toma.
#
# `zona-de-perigo` entrou na mesma leva e no mesmo grupo, e por um
# cuidado a mais: ela APAGA dados, e por isso NÃO toca a organização
# compartilhada do banco do CI — o seed dela cria duas organizações
# próprias e descartáveis, e o caso zera uma para provar que a outra
# sai inteira. Sem WAHA, Resend, Nuvemshop ou Redis.
# Cada item aqui tem o motivo MEDIDO, não presumido — e o gate cobra que a
# lista exista e some com as outras duas.
#
# `vps-fresh-onboarding` fica fora por DUAS razões, e só a primeira estava
# escrita — com o número errado, contradito 8 linhas abaixo neste mesmo
# arquivo, que já usava 17 como controle positivo:
#
# (1) 17 linhas dependem de WAHA/Resend/Nuvemshop/Redis
# (`grep -ciE "waha|resend|nuvemshop|redis"` → 17 linhas, 22 ocorrências).
#
# (2) `orgRow()` resolvia a organização com `.limit(1).single()` em
# `organizations`, SEM `where` — pressupondo banco de UMA organização
# só. O banco de uma parte é compartilhado entre as specs dela, e
# `signup-journey.spec.ts:32` cria uma segunda org sem cleanup: depois
# que ela roda, já há ≥2 na mesma parte.
#
# ⚠️ ESTA LINHA DIZIA QUE "o `.single()` falha". É FALSO, e a frase
# errada é o motivo de a classe ter sobrevivido em quatro lugares:
# ela descrevia o desfecho como barulho quando o desfecho é dano.
# Medido em 2026-09-04 contra PostgREST v14.10, três linhas na tabela:
#
# .select(...).limit(1).single() → error: null + a PRIMEIRA linha
# .select(...).single() → PGRST116 ("Cannot coerce…")
#
# O `.limit(1)` recorta ANTES da checagem de singularidade. Ou seja:
# o `beforeAll` desta suíte — que zera `onboarded_at`, apaga
# `ai_agents` e apaga `channel_sessions` — apagava dados de uma
# organização QUALQUER, em silêncio. Levantado por @Elevstudio-Dev
# (PR #559) depois de acontecer numa instalação de trabalho.
#
# A instância está corrigida (a org sai do dono do bootstrap) e a
# classe inteira virou gate: `tests/unit/e2e-nao-escolhe-a-primeira-linha`.
# O que continua valendo aqui é a razão (1): esta suíte não roda no CI
# por dependência externa, não por causa do rig de organização.
#
# Continua sendo a P0 da doutrina de QA Visual: `e2e` verde NÃO prova a
# jornada de instalação fresca, que é o produto que se vende.
#
# As CINCO specs da missão "Follow-up Vivo" (`followup-linguagem`,
# `followup-ramos`, `followup-dossie`, `followup-tempo-adaptativo`,
# `gatilho-de-etapa`) entraram nas listas de execução, e não aqui, porque a
# medição não dá motivo para excluí-las: grep de waha/resend/nuvemshop/redis
# devolve **0 em todas as cinco** (controle positivo: `vps-fresh-onboarding`
# devolve 17). Usam o endpoint de cron, como a `followup-journey` que já roda.
#
# ⚠️ O QUE NÃO ESTÁ MEDIDO: elas passam LOCALMENTE, e verde local não é verde
# de CI. Nenhuma delas rodou num job ainda — foram escritas nesta missão e
# ficaram fora de todas as listas, isto é, existiam e nenhum job as invocava.
# A primeira execução do `e2e` neste PR é que decide; se alguma reprovar por
# ambiente, o lugar dela é aqui embaixo COM o motivo medido, nunca uma
# exclusão preventiva "por garantia" — que é exatamente a dívida entrando
# pela porta de serviço que este bloco existe para barrar.
#
# `prova-painel-provedores` esteve aqui e SAIU: em 2026-08-08 o caso F3 exigia
# mais de 50 modelos da OpenRouter e o catálogo chegava ao CI com 2 linhas — a
# spec media a sorte do ambiente. `scripts/seed-e2e-catalogo-openrouter.ts`
# (main, 2026-08-09) semeia o catálogo no job, então a condição que a excluía
# deixou de existir e ela voltou para a lista de execução.
#
# `inbox-tempo-real` entrou aqui em 2026-08-26, no dia seguinte ao merge, e o
# motivo é medido nos dois eixos que importam — não é "está flaky, tira".
#
# (a) ELE NÃO DISCRIMINA O CONSERTO QUE DIZ GUARDAR. Bancada local fiel a
# este job (Supabase local pg17, `baseline.sql`, Realtime reiniciado
# depois do baseline, `next build` de produção, os mesmos seeds),
# revertendo SÓ `lib/supabase/browser.ts` para a versão anterior ao
# #327 — e conferindo no bundle que o conserto saiu
# (`grep -rc realtime-token .next/static` → 0, controle positivo
# `sb-deskcomm-auth` → 1):
#
# com o conserto → 1 passed
# sem o conserto → 1 passed
#
# Causa provável: `useMessagesRealtime` liga `refetchOnWindowFocus:
# true` e o `execFileSync` que injeta a mensagem devolve o foco à
# janela. Dois caminhos, mesma saída. Quem discrimina de verdade é
# `tests/unit/realtime-token-do-socket.test.ts`, no `verify`.
#
# (b) ELE REPROVA PR ALHEIO POR SORTE. Desde o conserto do locator:
# #327 `dd58ec7c` pass · #343 `b508208b` pass · #344 `4e5acb21` FAIL ·
# #344 rerun do MESMO sha FAIL. O #344 não toca uma linha de inbox,
# realtime ou socket.
#
# Um gate que reprova por moeda é pior que gate nenhum: ele treina o
# time a reexecutar em vez de olhar, e foi assim que a degradação
# silenciosa passou despercebida da primeira vez.
#
# A spec CONTINUA no repositório e continua sendo a cerca de ponta a
# ponta contra o sintoma relatado — roda local. Volta para a lista de
# execução quando a issue #347 entregar entrega determinística (o
# caminho provável é desligar o `refetchOnWindowFocus` durante a
# asserção, para sobrar um caminho só).
# ✅ `agenda-marcar-pela-tela` SAIU DAQUI — e o registro é o motivo.
#
# Ela nasceu `skip` pela DECISÃO 21.3: a frente 1 é API e não tem pixel
# próprio, então em vez de CITAR num relatório qual spec vai prová-la em
# tela, a spec é CRIADA com a condição de saída no cabeçalho.
#
# ⚠️ E ESSA CONDIÇÃO VENCEU SEM NINGUÉM VOLTAR PARA CONFERIR. Declarar a
# condição foi a coisa certa; o que faltou é que ninguém relê condição de
# saída. Medido quando alguém foi olhar:
#
# grep -n "^export async function" app/api/v1/agenda/agendamentos/route.ts
# # GET:95 POST:145 PATCH:149 DELETE:153
#
# Escrita e executada, ela achou CINCO defeitos que nenhuma leitura de
# código tinha achado — entre eles a tela dizendo "Marcado ✓" sem criar
# linha nenhuma, e um laço de requisições no painel de marcação.
#
# Entra na PARTE_2 ao lado de `agente-marca-consulta` porque as duas usam
# `seed-e2e-agenda.ts` — e cada uma o chama por si, para nenhuma depender
# da ordem em que a outra rodou.
# para dirigir, e a condição que a exclui deixa de existir. Enquanto isso,
# rodá-la no CI custaria um job para exercitar um `skip`.
#
# `agenda-conectar-google` entra pela MESMA 21.3 e pela frente 3 (Google
# Calendar BYO), que também não tem pixel: o botão "Conectar Google" e a
# faixa de estado moram na tela da Agenda, da frente 2.
#
# São DUAS condições de saída, e a primeira depende só de tela:
#
# (a) o caso SEM CHAVE sai daqui assim que a tela consumir
# `googleEstaConfigurado()` — ele não precisa de Google nenhum, porque
# o que prova é a instalação SEM `GOOGLE_CALENDAR_CLIENT_ID`, que é o
# estado de 100% dos deploys novos (DECISÃO 3.1, envs opcionais). É o
# caso que vem PRIMEIRO no arquivo de propósito: spec que só cobre o
# caminho feliz deixa sem prova justamente a primeira tela que todo
# self-hoster encontra;
#
# (b) o caso COM CHAVE precisa, além da tela, de uma conta Google de teste
# com consentimento pré-aprovado. Sem ela o job pararia num
# `accounts.google.com` esperando login humano — e é por isto que ela
# fica FORA do CI em vez de ficar sem existir.
# ─── PARTE 3 — nasceu em 2026-09-07, e o teto de 30 min é quem a pediu ───
#
# A parte 2 foi CANCELADA aos 30 min neste PR (job 101799282999), com 55 dos
# 57 arquivos terminados. Não foi defeito de spec: foi a suíte crescendo —
# o PR acrescenta 10 specs e todas as 10 caíram na parte 2 (47 → 57).
#
# Duas partes não davam mais para equilibrar com segurança. Medido pelo
# instrumento desta seção no próprio run cancelado: 24,9 min de relógio na
# parte 2 contra 13,7 min na parte 1, e o setup custa ~8,5 min IGUAIS em
# cada job. Dividir 39,7 min de Playwright em dois daria 28,4 min por job —
# 1,6 min abaixo do teto, que é folga que a próxima spec consome. Em três,
# dá ~22 min, a mesma folga que a parte 1 tem hoje.
#
# O corte é o ponto onde o relógio ACUMULADO da parte 2 chega à metade, na
# ordem em que o Playwright executa — não por contagem de arquivos, que
# esta seção já registra como proxy que inverte o sinal. Ficaram
# 32 arquivos (13.3 min) na parte 2 e 25 (11.7 min) aqui.
#
# A parte 3 é a CAUDA contígua da parte 2, e isso é deliberado: as specs de
# uma parte compartilham o banco sem reset, então cada vizinhança rompida é
# risco de contaminação. Uma cauda rompe UMA fronteira; um sorteio romperia
# todas. Os 3 arquivos que o cancelamento impediu de medir vêm junto, porque
# eles são justamente o fim da fila.
SPECS_PARTE_3: >-
autonomia-assistida.spec.ts
roteamento-por-canal.spec.ts
central-avisos-destino.spec.ts
agenda-presenca-recuperacao.spec.ts
agenda-google-sync.spec.ts
agenda-google-meet.spec.ts
encerramento-atendimento.spec.ts
interface-por-vinculo.spec.ts
olhar-telas-do-epico.spec.ts
pipelines-gestao.spec.ts
qa-agente-usa-as-maos.spec.ts
qa-selo-no-funil-usado.spec.ts
qa-telas-descobertas-w4.spec.ts
retorno-anti-morte.spec.ts
webhooks.spec.ts
marca-logo.spec.ts
historico-de-captacao.spec.ts
inbox-quem-manda.spec.ts
inbox-rotulo-de-origem.spec.ts
troca-de-organizacao-tem-volta.spec.ts
organizacoes-criacao-convite-e-cache.spec.ts
suporte-temporario.spec.ts
i18n-espanhol-na-tela.spec.ts
inbox-abas-espelham-o-comando.spec.ts
moeda-da-organizacao.spec.ts
zona-de-perigo-apaga-dados-de-teste.spec.ts
central-avisos-resolver-em-lote.spec.ts
voz-desligada-por-padrao.spec.ts
FORA_DO_CI: >-
vps-fresh-onboarding.spec.ts
inbox-tempo-real.spec.ts
cadastro-sem-confirmacao-de-email.spec.ts
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/preparar-node
# O teto do preâmbulo mora aqui porque passo de composite action não
# aceita `timeout-minutes`. Razão e medições: o cabeçalho da action.
timeout-minutes: 5
- uses: supabase/setup-cli@v3
with:
version: latest
# Sobe Postgres + Auth (GoTrue) + PostgREST + Storage. Auth de verdade é
# requisito: os specs de login exercitam o GoTrue, não um mock.
#
# A pasta de migrations sai do caminho ANTES do start: a cadeia não sobe
# em banco novo (quebra na 0010, `alter table public.contacts` sobre uma
# tabela que ainda não existe) — está no CLAUDE.md e foi confirmado neste
# CI. O que o self-hoster aplica é o `baseline.sql`, então é ele que o
# e2e aplica: o banco do teste fica igual ao de quem instalou o produto.
- name: Subir Supabase local (sem a cadeia de migrations)
run: |
mv supabase/migrations /tmp/migrations-off
mkdir -p supabase/migrations
supabase start
# O baseline é um dump `--schema-only`: ele USA os tipos das extensões
# (public.vector, citext, gin_trgm_ops) mas não as cria. O stack local do
# Supabase traz os binários e não as habilita — mesmo prelúdio que o
# scripts/test-db.sh faz para o Postgres cru, só a parte de extensões
# (roles e schema auth já vêm prontos aqui).
- name: Habilitar extensões exigidas pelo baseline
run: |
psql "postgresql://postgres:postgres@127.0.0.1:54322/postgres" -v ON_ERROR_STOP=1 -q <<'SQL'
create schema if not exists extensions;
create extension if not exists "uuid-ossp" with schema extensions;
create extension if not exists pgcrypto with schema extensions;
create extension if not exists vector with schema public;
create extension if not exists citext with schema public;
create extension if not exists pg_trgm with schema public;
SQL
- name: Aplicar o baseline.sql (o mesmo que o install.sh do kit aplica)
run: |
psql "postgresql://postgres:postgres@127.0.0.1:54322/postgres" \
-v ON_ERROR_STOP=1 -q -f supabase/baseline.sql
# ⚠️ O REALTIME PRECISA REINICIAR DEPOIS DO BASELINE — senão ele assina e
# não entrega, em silêncio.
#
# A ordem acima é `supabase start` (que sobe o Realtime) e SÓ DEPOIS o
# baseline, que é quem adiciona `messages`, `conversations`, `crm_leads` e
# as demais à publication `supabase_realtime`. O Realtime já subiu com a
# publication sem essas tabelas e não as reconhece depois.
#
# Medido no PR #327, com o defeito atravessando três rodadas: o script de
# apoio confirmou a gravação (`[e2e-chega-mensagem] entregue em 6f5fd1f2…`)
# e o snapshot da página no mesmo instante mostrava "Nenhuma mensagem nesta
# conversa." — gravado no banco, nunca entregue à tela. Canal anônimo e
# canal sem publication falham do MESMO jeito: respondem SUBSCRIBED e
# calam, que é a razão de isto ter custado três rodadas de 15 minutos.
#
# Na VPS o problema não existe: lá o `install.sh` aplica o baseline e só
# então o compose sobe os serviços. Reiniciar aqui é o que faz o ambiente
# do teste refletir o de quem instalou.
- name: Reiniciar o Realtime (a publication nasceu no passo anterior)
run: |
IDS=$(docker ps -q --filter name=supabase_realtime)
if [ -z "$IDS" ]; then
echo "::error::Nenhum container supabase_realtime — o stack não subiu como esperado."
exit 1
fi
docker restart $IDS
# Espera ficar saudável: seguir sem isso devolve o mesmo silêncio que
# este passo existe para eliminar, só que mais tarde e sem explicação.
for i in $(seq 1 30); do
estado=$(docker inspect -f '{{.State.Health.Status}}' $IDS 2>/dev/null || echo desconhecido)
[ "$estado" = "healthy" ] && { echo "realtime saudável em ~$((i*2))s"; break; }
sleep 2
done
docker inspect -f '{{.State.Health.Status}}' $IDS
- name: Exportar credenciais do stack local
run: |
supabase status -o env > /tmp/sb.env
{
echo "NEXT_PUBLIC_SUPABASE_URL=$(grep '^API_URL=' /tmp/sb.env | cut -d'"' -f2)"
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=$(grep '^ANON_KEY=' /tmp/sb.env | cut -d'"' -f2)"
echo "SUPABASE_SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' /tmp/sb.env | cut -d'"' -f2)"
} >> "$GITHUB_ENV"
# O `.env.e2e` é o ambiente da suíte, e ele NASCE aqui — não é opcional.
#
# `playwright.config.ts` passou a exigi-lo e a falhar alto quando falta,
# de propósito: sem ele o `next start` carrega o `.env.local`, que num
# checkout de trabalho aponta para PRODUÇÃO. O modo de falha que isso
# substitui é a suíte escrever organizações e usuários de teste no banco
# real, passando verde.
#
# No CI o `.env.local` aponta para o Supabase local, então o risco não é o
# mesmo — mas o contrato é único, e um workflow que contorna o gate seria
# o começo de ele não valer para ninguém.
- name: Gerar o .env.e2e (ambiente da suíte)
run: pnpm e2e:env
# E publica o arquivo no ambiente do JOB, para que servidor e testes leiam
# literalmente a mesma fonte.
#
# Sem isto havia duas: o `.env.e2e` (que o playwright.config injeta no
# `next start`) e um bloco `env:` copiado à mão em cada passo de teste. As
# duas divergiram — `INTERNAL_SECRET` valia `e2e-placeholder…` no servidor
# e `ci-placeholder…` no processo de teste. Sintoma medido: 401 em toda
# chamada a rota interna, 8 specs vermelhas (6 de system-update no
# heartbeat, o drain do anti-SSRF, e o invite-lifecycle).
#
# Medido no Playwright 1.5x: `webServer.env` MESCLA com o ambiente herdado
# (uma var só do shell chega ao servidor) e VENCE nas colisões. Ou seja,
# copiar valores à mão nunca ia empatar com o arquivo — só uma fonte
# empata.
#
# De brinde, o processo de teste passa a ver as chaves de CIFRA de verdade
# (32 bytes, geradas pelo script) em vez do rótulo de 21 caracteres que o
# bloco `env:` fixava e que o app recusa.
- name: Publicar o .env.e2e no ambiente do job
run: grep -vE '^[[:space:]]*(#|$)' .env.e2e >> "$GITHUB_ENV"
# `pnpm e2e:build`, não `pnpm build`: as três `NEXT_PUBLIC_*` são embutidas
# no BUNDLE durante o build. Buildar com um env e trocar só no `next start`
# deixaria a URL errada dentro do JavaScript que roda no browser — servidor
# falando com um banco e cliente com outro, no mesmo teste.
- name: Build de produção (com o ambiente do E2E)
run: pnpm e2e:build
env:
NEXT_TELEMETRY_DISABLED: "1"
# `off`, NÃO vazio. Vazio seleciona o Sentry DA COMUNIDADE — está
# escrito em lib/sentry/dsn.ts:8-10, e é o default de propósito num
# produto self-host. Só "off" desliga.
#
# Sem isto, o SDK do browser tuneliza por /monitoring (next.config.ts:71)
# para o Sentry real do projeto, que devolve 429 por cota. Os 429 caem
# no console, e `olhar-telas-do-epico` — a ÚNICA spec que afirma "não
# cospe erro no console" — reprova nas 7 telas de uma vez.
# Medido em 2026-08-10 capturando a URL: /monitoring?o=4509908078559232,
# que é o org id do DEFAULT_SENTRY_DSN (lib/sentry/dsn.ts:17).
SENTRY_DSN: "off"
- name: Instalar o browser
run: pnpm exec playwright install --with-deps chromium
# `.env.local` é CÓPIA do `.env.e2e`, não uma terceira redação dele.
#
# O arquivo precisa existir porque alguns seeds leem `.env.local` do disco,
# não de `process.env`. Ele era montado à mão aqui, com os valores
# redigitados — e foi a terceira fonte do mesmo `INTERNAL_SECRET`, com o
# terceiro valor. Copiar não pode divergir; redigitar já divergiu.
#
# No CI as duas apontam para o mesmo stack local, então a cópia não
# reintroduz o risco que o `.env.e2e` existe para evitar (o `.env.local` de
# um checkout de trabalho apontar para produção).
#
# O seed cria 1 org + 4 usuários com os 4 papéis e um TOTP verified de
# secret conhecido no admin — sem ele, todo spec que loga fica de fora, que
# era a maior parte da suíte.
- name: Semear credenciais de teste
run: |
cp .env.e2e .env.local
pnpm exec tsx scripts/seed-e2e-credentials.ts
# `SUPABASE_DB_URL` no `.env.local` (e não só no `env:` do passo de teste):
# `seed-e2e-escalacao.ts` abre um `pg.Pool` direto, e o `.env.local` é a
# única fonte que ele lê. Sem a linha, o `pg` cai no default 5432 e morre
# com ECONNREFUSED — o stack local do Supabase publica o Postgres na 54322.
# Medido: foi exatamente assim que a primeira execução deste passo falhou.
#
# A maioria das specs roda o próprio seed num `beforeAll`. Estas TRÊS não
# — ou o cabeçalho manda rodar à mão, ou a fixture é de outra spec.
#
# `seed-e2e-followup-agent` (credencial de IA + sessão de canal) é o caso
# sutil: `followup-builder` o roda no próprio `beforeAll`, mas
# `agente-novo-e-uso` PRECISA das mesmas fixtures e vem ANTES na ordem
# alfabética — em banco fresco os comboboxes de credencial e canal abrem
# com a opção desabilitada e o clique estoura por timeout. Medido: o
# ensaio local passou porque o banco tinha as fixtures de uma rodada
# anterior; o CI, fresco, reprovou. Rodar aqui torna a precondição
# explícita em vez de depender da ordem dos arquivos.
#
# `--env-file` só na segunda, e a diferença não é capricho: o seed de
# capacidades importa `lib/env.ts` (via lib/ai/embed.ts), que lê
# `process.env`; o de escalação parseia o `.env.local` por conta própria.
# Sem a flag o de capacidades morre na validação Zod das 3 vars do Supabase.
- name: Semear as fixtures que as specs não semeiam sozinhas
run: |
pnpm exec tsx scripts/seed-e2e-escalacao.ts
pnpm exec tsx --env-file=.env.local scripts/seed-e2e-capacidades-ausentes.ts
pnpm exec tsx scripts/seed-e2e-followup-agent.ts
# O catálogo da OpenRouter não existe em banco fresco (o baseline
# semeia só anthropic/openai/google; os demais vêm do cron diário, que
# busca na internet). Sem esta linha, `prova-painel-provedores` mede a
# sorte do ambiente em vez da tela.
pnpm exec tsx scripts/seed-e2e-catalogo-openrouter.ts
# O que ficou DE FORA (e por quê) é declarado no passo de summary —
# cobertura parcial silenciosa se lê como cobertura total.
#
# `next start` roda com NODE_ENV=production, e aí toda var marcada
# `required()` no lib/env.ts vira obrigatória, inclusive as de serviço que
# estes specs nem tocam. Os valores abaixo são PLACEHOLDERS FALSOS, só
# para o boot passar da validação Zod — nenhum segredo real entra aqui.
# DUAS INVOCAÇÕES, e não uma — o motivo é medido, não estético.
#
# `lib/auth/rate-limit.ts` limita login a 60 por IP a cada 300s. No CI todo
# spec loga do mesmo 127.0.0.1, e ao subir a suíte de 15 para 29 specs o
# pico passou a ser 69–73 tentativas na janela: a suíte estourava o próprio
# teto do produto. O sintoma não é honesto — o login simplesmente não
# redireciona, e a spec sorteada pelo momento da saturação é a que falha.
# Foi assim que `risk-radar` caiu na main e `rbac-roles` num PR, sem
# nenhuma das duas ter mudado.
#
# Sem Upstash configurado, o limitador cai para memória DO PROCESSO (o log
# diz: "redis incr failed; falling back to in-memory"), e o
# `reuseExistingServer: false` do playwright.config.ts faz cada invocação
# subir o SEU `next start`. Duas invocações = dois processos = dois
# contadores, e o pico por processo volta para ~40.
#
# A alternativa seria afrouxar o teto por env — mexer num controle de
# segurança para acomodar o teste, que é a troca errada.
#
# `--workers=1` não é preciosismo: os specs compartilham UMA conta admin
# com MFA, e código TOTP é de uso único por janela no GoTrue. Com dois
# workers, dois specs gerando o mesmo código no mesmo intervalo de 30s
# fazem o segundo ser recusado como replay. Foi o que derrubava a
# `reset-password-mfa` (issue #80): ela passa sozinha e falha em paralelo.
- name: E2E — parte ${{ matrix.parte }} de 3
run: |
set -euo pipefail
# A lista sai da matrix, e as duas variáveis continuam existindo no
# `env:` do job porque é DELAS que `e2e-cobertura-completa.test.ts` lê
# a cobertura. Ler por indireção aqui e não duplicar a lista mantém
# UMA fonte por parte.
case "${{ matrix.parte }}" in
1) LISTA="$SPECS_PARTE_1" ;;
2) LISTA="$SPECS_PARTE_2" ;;
3) LISTA="$SPECS_PARTE_3" ;;
*) echo "::error::parte ${{ matrix.parte }} sem lista"; exit 1 ;;
esac
# Falha alto se a variável vier vazia: sem isto, um typo no nome faria
# o playwright rodar a suíte INTEIRA (ou nenhuma) e o job ficaria
# verde medindo outra coisa.
[ -n "$LISTA" ] || { echo "::error::lista da parte ${{ matrix.parte }} vazia"; exit 1; }
# `--reporter=list` NÃO é preferência de leitura: é o INSTRUMENTO que
# mede o custo de cada spec, e sem ele a partição das listas volta
# a ser chutada.
#
# O reporter default em CI é o `dot`, e ele destrói justamente o dado
# que decide o balanceamento: os pontos saem em rajada, esvaziados só
# quando outro processo escreve na saída, e o timestamp do Actions é
# por LINHA. Medido no run 33820766602 (verde): a parte 1 despejou
# **37 pontos numa linha só**, cobrindo 4,9 min — 37 testes que se
# tornam indistinguíveis entre si. Reconstruir tempo por spec a partir
# disso é o terceiro proxy ruim que este arquivo já viu, e os dois
# anteriores INVERTERAM o sinal da medição (ver o bloco de
# `SPECS_PARTE_1` abaixo).
#
# Com o `list`, cada teste vira uma linha com caminho e duração
# (`✓ 12 [chromium] › tests/e2e/x.spec.ts:9:5 › título (3.4s)`), e a
# soma por arquivo sai de um `gh run view <id> --log`. Custo medido:
# ~240 linhas a mais de log, zero segundo de execução.
pnpm exec playwright test --workers=1 $LISTA --reporter=list
env:
# Só o que NÃO vem do `.env.e2e`: isto é ajuste de CI, não config do
# produto. CI = 1 IP para todos os specs, e o teto de produção
# (60/5min) é irreal aqui. O teto por CONTA (5 falhas) NÃO muda — é
# ele que barra brute force.
AUTH_RATE_LIMIT_LOGIN_IP: "1000"
# ver o comentário do passo de build: vazio NÃO desliga.
SENTRY_DSN: "off"
- uses: actions/upload-artifact@v7
if: failure()
with:
name: playwright-report-parte-${{ matrix.parte }}
path: |
playwright-report/
test-results/
retention-days: 7
# O NOME DESTE JOB É O CHECK OBRIGATÓRIO, e é por isso que ele existe.
#
# A branch protection da `main` exige um check chamado exatamente `e2e`
# (medido: verify, build-and-size, invariants, e2e, imagens-ok). Jobs de
# matrix se chamam `e2e-parte (1)` e `e2e-parte (2)` — se o job das partes
# tivesse ficado com o nome `e2e`, a proteção passaria a esperar para sempre
# um check que nenhum run produz, e NENHUM PR mergearia. Este agregador é o
# mesmo padrão do `imagens-ok` em `publish-image.yml`.
e2e:
if: always()
needs: [e2e-parte]
runs-on: ubuntu-latest
# Nenhuma permissão: este job só lê o resultado de `needs` e escreve o
# summary. Sem o bloco, o token herdaria o default do REPOSITÓRIO, que vive
# fora do repo e muda num clique.
permissions: {}
steps:
- name: Falhar se qualquer parte não passou
run: |
echo "e2e-parte: ${{ needs.e2e-parte.result }}"
# `success` é o único desfecho aceito. `skipped` também reprova, de
# propósito: parte pulada não mediu nada, e ler isso como aprovação é
# o modo de falha que o `imagens-ok` documenta.
[ "${{ needs.e2e-parte.result }}" = "success" ]
- uses: actions/checkout@v7
if: always()
- name: Declarar o que este job ainda NÃO cobre
if: always()
run: |
set -euo pipefail
# CONTA, não afirma. A versão anterior deste passo era prosa digitada à
# mão e dizia "32 de 33" com 39 arquivos no disco — a única coisa que
# existia para declarar a lacuna estava, ela mesma, desatualizada.
#
# As listas saem do PRÓPRIO workflow, com recorte por bloco. Contar com
# um `grep` no arquivo inteiro daria número MAIOR: os nomes aparecem
# também em comentários, e foi assim que a contagem antiga mentiu.
#
# ⚠️ O RECORTE PARA DE SER POR RANGE DE `sed` — ele lia COMENTÁRIO.
#
# A versão anterior era `sed -n "/^ $1: >-/,/^ [A-Z_0-9]*:/p"`,
# e o `[A-Z_0-9]` incluía DÍGITO de propósito: sem ele o range de
# `SPECS_PARTE_1` não parava em `SPECS_PARTE_2` (que tem `2` no nome) e
# seguia até `FORA_DO_CI`, devolvendo a união das duas como se fosse a
# parte 1. Medido antes daquele conserto: 67 em vez de 29.
#
# O que aquele conserto não alcançou é que o range NÃO PARA no fim do
# bloco `>-`: ele para na próxima CHAVE, e entre uma coisa e outra
# moram os comentários — que citam nome de spec o tempo todo. Medido
# neste arquivo, contra a lista de verdade:
#
# SPECS_PARTE_1 real 30 · sed 31 (extra: `marca-logo`, citada no comentário)
# SPECS_PARTE_2 real 43 · sed 44 (extra: `signup-journey`, idem)
# FORA_DO_CI real 2 · sed 3 (extra: um exemplo de saída do reporter)
#
# Nas duas primeiras o erro se anula na CONTA — as extras já estão na
# outra lista, e `RODOU` é união. Na terceira, não: `FORA_DO_CI` é a
# única lista impressa NOME A NOME, e o range dela vai até o fim do
# arquivo (nenhuma chave de 6 espaços vem depois). Qualquer comentário
# abaixo dela que cite um `.spec.ts` vira uma linha a mais em "Não
# rodou", com o motivo alheio colado — a declaração de cobertura
# afirmando falso sobre si mesma, que é a classe inteira que este
# passo existe para matar.
#
# O `awk` abaixo recorta o BLOCO: entra na chave, imprime enquanto as
# linhas forem continuação (8 espaços), e sai na primeira que não for
# — comentário de 6 espaços inclusive.
bloco() { awk -v k="$1" '
index($0, " " k ": >-") == 1 { dentro = 1; next }
dentro && substr($0, 1, 8) == " " && substr($0, 9, 1) != " " { print; next }
dentro { exit }
' .github/workflows/e2e.yml | grep -oE "[a-z0-9-]+\.spec\.ts" | sort -u; }
NO_DISCO=$(ls tests/e2e/*.spec.ts | wc -l | tr -d " ")
RODOU=$( { bloco SPECS_PARTE_1; bloco SPECS_PARTE_2; bloco SPECS_PARTE_3; } | sort -u | wc -l | tr -d " ")
FORA_LISTA=$(bloco FORA_DO_CI)
FORA=$(echo "$FORA_LISTA" | grep -c . || true)
# E o recorte passa a ter CONTROLE, não só um piso.
#
# `tests/unit/e2e-cobertura-completa.test.ts` já garante que toda spec
# do disco está em exatamente uma das três listas, sem duplicata e sem
# fantasma. Logo `RODOU + FORA` TEM de dar `NO_DISCO` — e quando não
# der, o recorte leu a mais ou a menos. Sem esta linha, ler a mais é
# invisível: o passo imprime um número maior e segue verde, que é
# exatamente como a contagem antiga mentiu por três versões seguidas.
if [ "$((RODOU + FORA))" -ne "$NO_DISCO" ]; then
echo "::error::recorte das listas divergiu — rodou=$RODOU fora=$FORA disco=$NO_DISCO"
exit 1
fi
# Falha alto se o recorte parou de casar: listas vazias fariam este
# passo anunciar "0 de N" como se fosse notícia, em vez de erro.
[ "$RODOU" -gt 10 ] || { echo "::error::o recorte das listas do workflow parou de casar"; exit 1; }
{
echo "## E2E — cobertura deste job"
echo ""
echo "**Rodou: ${RODOU} de ${NO_DISCO} specs**, em partes paralelas."
echo ""
echo "**Não rodou (${FORA}):**"
for s in $FORA_LISTA; do echo "- \`${s}\` — precisa de WAHA + Redis + Resend + Nuvemshop (P0 da doutrina de QA Visual)"; done
echo ""
echo "A soma é conferida mecanicamente por \`tests/unit/e2e-cobertura-completa.test.ts\`:"
echo "spec no disco que não esteja em nenhuma das três listas reprova o build."
} >> "$GITHUB_STEP_SUMMARY"