fix(seguranca): relatório da comunidade — vazamento entre organizações, RBAC nas policies, MFA de sessão, open redirect e SSRF #328
Workflow file for this run
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
| 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 — hoje 29 de 33. | |
| # | |
| # 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 2". | |
| # | |
| # NÃO-BLOQUEANTE por ausência, não por mordaça: `e2e` não está na lista de | |
| # checks obrigatórios da branch protection, então falhar aqui não segura merge. | |
| # O job em si falha honestamente — a primeira versão deste arquivo 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. | |
| # | |
| # Quando N execuções seguidas passarem limpas, adicionar `e2e` aos obrigatórios. | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| jobs: | |
| e2e: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| # ───────────────────────────────────────────────────────────────────────── | |
| # 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. | |
| # ───────────────────────────────────────────────────────────────────────── | |
| # | |
| # ═══ 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 nas duas 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. | |
| env: | |
| SPECS_PARTE_1: >- | |
| smoke.spec.ts auth.spec.ts error-pages.spec.ts | |
| password-recovery.spec.ts signup-journey.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 | |
| 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 | |
| SPECS_PARTE_2: >- | |
| agente-novo-e-uso.spec.ts agente-organiza-operacao.spec.ts | |
| central-de-avisos-capacidades.spec.ts escalacao-ciclo.spec.ts | |
| navegacao.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 | |
| followup-builder.spec.ts followup-queue.spec.ts | |
| distribuicao-atendimento.spec.ts followup-journey.spec.ts | |
| webhooks.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 | |
| # 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`: 8 referências a WAHA/Resend/Nuvemshop/Redis. É a P0 | |
| # da doutrina de QA Visual e fica fora por dependência de infra. | |
| # | |
| # 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. | |
| FORA_DO_CI: >- | |
| vps-fresh-onboarding.spec.ts | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: pnpm/action-setup@v6 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 22 | |
| cache: pnpm | |
| - run: pnpm install --frozen-lockfile | |
| - 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 | |
| - 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 1 de 2 | |
| run: pnpm exec playwright test --workers=1 $SPECS_PARTE_1 | |
| 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" | |
| - name: E2E — parte 2 de 2 (processo novo, contador de login zerado) | |
| run: pnpm exec playwright test --workers=1 $SPECS_PARTE_2 | |
| 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" | |
| - name: Declarar o que este job ainda NÃO cobre | |
| if: always() | |
| run: | | |
| # 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. | |
| NO_DISCO=$(ls tests/e2e/*.spec.ts | wc -l | tr -d ' ') | |
| RODOU=$(echo $SPECS_PARTE_1 $SPECS_PARTE_2 | wc -w | tr -d ' ') | |
| FORA=$(echo $FORA_DO_CI | wc -w | tr -d ' ') | |
| { | |
| echo "## E2E — cobertura deste job" | |
| echo "" | |
| echo "**Rodou: ${RODOU} de ${NO_DISCO} specs.**" | |
| echo "" | |
| echo "**Não rodou (${FORA}):**" | |
| for s in $FORA_DO_CI; 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" | |
| - uses: actions/upload-artifact@v7 | |
| if: failure() | |
| with: | |
| name: playwright-report | |
| path: | | |
| playwright-report/ | |
| test-results/ | |
| retention-days: 7 |