Skip to content

Commit 94ab896

Browse files
melgarafaelclaude
andcommitted
fix(e2e): o CI cria o .env.e2e que a suíte passou a exigir
O PR protegeu a suíte contra escrever em produção — `playwright.config.ts` exige um `.env.e2e` e falha alto quando falta. A proteção está certa; o que saiu incompleto foi a outra ponta: `.github/workflows/e2e.yml` não foi tocado. O job seguiu rodando `pnpm build` e chamando a suíte sem gerar nada, e o e2e morreu em `Falta o .env.e2e` depois de 3m48s. Três consertos, todos medidos: 1. O workflow gera o `.env.e2e` (`pnpm e2e:env`) depois de o stack local subir, e builda com `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 do browser. Medido: `==> OK (controle): o host local (127.0.0.1:54321) ESTÁ no bundle`. 2. `gerar-env-e2e.sh` usa o `supabase` do PATH quando existe (é o que o CI instala via supabase/setup-cli) e cai no `npx` só na máquina do dev. A CLI não é dependência deste projeto — insistir no `npx` custaria um download do registry em cada uma das três chamadas. 3. O heredoc que escreve o `.env.e2e` não é quotado, e um comentário dentro dele tem crases. O shell tentou EXECUTAR o que estava entre elas: scripts/gerar-env-e2e.sh: linha 79: credential_decrypt_failed: comando não encontrado Saía em stderr e o arquivo gerado perdia exatamente o identificador que o comentário existe para nomear (`# agente morria em , o que aparecia`). Varri o heredoc inteiro: as crases eram a única expansão indesejada, as `$VAR` são todas de propósito. A guarda nova é sobre a ORDEM, não a presença: `tests/unit/ e2e-workflow-honra-o-env.test.ts` casa quem gera o arquivo contra quem o consome e exige que o índice do primeiro seja menor. Sabotei as quatro propriedades uma a uma — passo removido, ordem invertida, `pnpm build` de volta, e a exigência sumindo do playwright.config (guarda de vacuidade) — e cada uma vermelha o caso certo. gov:verify: 0 erros de lint, 286 arquivos / 2931 testes (2927 + 4). e2e:build: exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HstH7nNmrTvCtZsasppeHj
1 parent 6e0ac6d commit 94ab896

3 files changed

Lines changed: 133 additions & 5 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,26 @@ jobs:
8585
echo "SUPABASE_SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' /tmp/sb.env | cut -d'"' -f2)"
8686
} >> "$GITHUB_ENV"
8787
88-
- name: Build de produção
89-
run: pnpm build
88+
# O `.env.e2e` é o ambiente da suíte, e ele NASCE aqui — não é opcional.
89+
#
90+
# `playwright.config.ts` passou a exigi-lo e a falhar alto quando falta,
91+
# de propósito: sem ele o `next start` carrega o `.env.local`, que num
92+
# checkout de trabalho aponta para PRODUÇÃO. O modo de falha que isso
93+
# substitui é a suíte escrever organizações e usuários de teste no banco
94+
# real, passando verde.
95+
#
96+
# No CI o `.env.local` aponta para o Supabase local, então o risco não é o
97+
# mesmo — mas o contrato é único, e um workflow que contorna o gate seria
98+
# o começo de ele não valer para ninguém.
99+
- name: Gerar o .env.e2e (ambiente da suíte)
100+
run: pnpm e2e:env
101+
102+
# `pnpm e2e:build`, não `pnpm build`: as três `NEXT_PUBLIC_*` são embutidas
103+
# no BUNDLE durante o build. Buildar com um env e trocar só no `next start`
104+
# deixaria a URL errada dentro do JavaScript que roda no browser — servidor
105+
# falando com um banco e cliente com outro, no mesmo teste.
106+
- name: Build de produção (com o ambiente do E2E)
107+
run: pnpm e2e:build
90108
env:
91109
NEXT_TELEMETRY_DISABLED: "1"
92110

scripts/gerar-env-e2e.sh

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ CHAVE_CPF=""; CHAVE_WAHA=""; CHAVE_AI=""
2828

2929
cd "$(dirname "$0")/.."
3030

31-
if ! npx supabase status >/dev/null 2>&1; then
31+
# `supabase` do PATH quando existe (é o que o CI instala, via supabase/setup-cli),
32+
# `npx supabase` como plano B para a máquina do dev. Insistir no `npx` custaria um
33+
# download do registry a cada uma das três chamadas abaixo — a CLI não é
34+
# dependência deste projeto.
35+
SUPABASE="supabase"
36+
command -v supabase >/dev/null 2>&1 || SUPABASE="npx supabase"
37+
38+
if ! $SUPABASE status >/dev/null 2>&1; then
3239
echo "==> O Supabase local não está de pé. Rode 'npx supabase start' antes." >&2
3340
exit 1
3441
fi
@@ -45,7 +52,7 @@ fi
4552
[ "${#CHAVE_WAHA}" -ge 44 ] || CHAVE_WAHA="$(openssl rand -base64 32)"
4653
[ "${#CHAVE_AI}" -ge 44 ] || CHAVE_AI="$(openssl rand -base64 32)"
4754

48-
ENVOUT="$(npx supabase status -o env 2>/dev/null)"
55+
ENVOUT="$($SUPABASE status -o env 2>/dev/null)"
4956
ler() { printf '%s\n' "$ENVOUT" | grep "^$1=" | cut -d= -f2- | tr -d '"'; }
5057

5158
API_URL="$(ler API_URL)"
@@ -86,7 +93,7 @@ INTERNAL_SECRET=e2e-placeholder-nao-e-segredo
8693
# As três abaixo são chaves de CIFRA de verdade: o app exige 32 bytes e recusa
8794
# um rótulo. Medido — com o placeholder, criar credencial de IA devolvia 500
8895
# ("AI_CRED_AES_KEY deve ter exatamente 32 bytes (lido: 21)") e todo run do
89-
# agente morria em `credential_decrypt_failed`, o que aparecia como "o modelo
96+
# agente morria em \`credential_decrypt_failed\`, o que aparecia como "o modelo
9097
# não respondeu". Geradas na hora: são de teste, não precisam sobreviver.
9198
CPF_ENCRYPTION_KEY=$CHAVE_CPF
9299
WAHA_BYO_ENCRYPTION_KEY=$CHAVE_WAHA
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* QUEM EXIGE O `.env.e2e` E QUEM O CRIA TÊM DE SER O MESMO CONTRATO.
3+
*
4+
* ## O defeito
5+
*
6+
* `playwright.config.ts` passou a EXIGIR um `.env.e2e` e a falhar alto quando
7+
* ele falta — a proteção certa, pelo motivo certo: sem esse arquivo o
8+
* `next start` carrega o `.env.local`, que num checkout de trabalho aponta para
9+
* PRODUÇÃO, e a suíte semeia organizações e usuários de teste no banco real
10+
* passando verde.
11+
*
12+
* O que saiu incompleto foi a outra ponta: o refactor mudou o contrato do
13+
* ambiente e não mexeu em `.github/workflows/e2e.yml`. O job seguiu rodando
14+
* `pnpm build` e chamando a suíte sem gerar nada, e o e2e do PR morreu em
15+
* `Falta o .env.e2e` — 3m48s de CI para descobrir uma dependência que ninguém
16+
* declarou em lugar nenhum.
17+
*
18+
* ## Por que uma guarda estática, e não "rodar o workflow"
19+
*
20+
* Este é um defeito de ACOPLAMENTO entre dois arquivos: um exige, o outro
21+
* provê. A pergunta é sobre a relação, e ela é decidível lendo os dois. O único
22+
* jeito de descobri-lo dinamicamente é gastar um job inteiro de CI, que é
23+
* exatamente o custo que se está tentando não pagar de novo.
24+
*
25+
* ## O que se guarda
26+
*
27+
* A ORDEM, não a presença: existir um passo que gera o `.env.e2e` não adianta
28+
* se ele vier depois de quem consome. Por isso a asserção é sobre índices.
29+
*/
30+
import { describe, expect, it } from "vitest";
31+
import * as fs from "node:fs";
32+
import * as path from "node:path";
33+
34+
const RAIZ = path.resolve(__dirname, "../..");
35+
36+
const workflow = fs.readFileSync(path.join(RAIZ, ".github/workflows/e2e.yml"), "utf8");
37+
const config = fs.readFileSync(path.join(RAIZ, "playwright.config.ts"), "utf8");
38+
const packageJson = JSON.parse(fs.readFileSync(path.join(RAIZ, "package.json"), "utf8")) as {
39+
scripts: Record<string, string>;
40+
};
41+
42+
/**
43+
* Linhas de `run:` do workflow, na ordem em que o job as executa. É a unidade
44+
* certa: o que importa é qual comando roda antes de qual, não como o passo se
45+
* chama.
46+
*/
47+
const COMANDOS: string[] = workflow
48+
.split("\n")
49+
.filter((l) => !l.trim().startsWith("#"))
50+
.map((l) => l.trim());
51+
52+
/** Índice do primeiro comando que casa, ou -1. */
53+
function primeiroIndice(padrao: RegExp): number {
54+
return COMANDOS.findIndex((l) => padrao.test(l));
55+
}
56+
57+
const GERA_ENV = /pnpm (e2e:env|exec .*gerar-env-e2e)|bash scripts\/gerar-env-e2e\.sh/;
58+
const CONSOME_ENV = /pnpm (test:e2e|e2e:build)|playwright test/;
59+
60+
describe("o workflow do e2e honra o contrato de ambiente que a suíte exige", () => {
61+
it("a suíte de fato exige o .env.e2e (guarda de vacuidade)", () => {
62+
// Sem isto, o dia em que `playwright.config.ts` parar de exigir o arquivo,
63+
// os casos abaixo continuariam verdes vigiando uma regra que não existe
64+
// mais — e o teste viraria peso morto que ninguém percebe.
65+
expect(config).toMatch(/\.env\.e2e/);
66+
expect(config, "a exigência tem de FALHAR, não avisar").toMatch(/throw new Error/);
67+
});
68+
69+
it("o script que gera o arquivo existe e está no package.json", () => {
70+
expect(packageJson.scripts["e2e:env"]).toBeTruthy();
71+
expect(fs.existsSync(path.join(RAIZ, "scripts/gerar-env-e2e.sh"))).toBe(true);
72+
});
73+
74+
it("algum passo do workflow gera o .env.e2e ANTES de qualquer passo que o consuma", () => {
75+
const gera = primeiroIndice(GERA_ENV);
76+
const consome = primeiroIndice(CONSOME_ENV);
77+
78+
// Sem consumidor nenhum o caso seria vácuo: nada a ordenar.
79+
expect(consome, "nenhum passo do workflow roda a suíte — o padrão envelheceu").toBeGreaterThan(
80+
-1,
81+
);
82+
expect(
83+
gera,
84+
"nenhum passo gera o .env.e2e; a suíte vai morrer em 'Falta o .env.e2e'",
85+
).toBeGreaterThan(-1);
86+
expect(
87+
gera,
88+
"o .env.e2e é gerado DEPOIS de quem precisa dele — existir não basta, tem de vir antes",
89+
).toBeLessThan(consome);
90+
});
91+
92+
it("o build do job é o que embute as NEXT_PUBLIC_* do ambiente de teste", () => {
93+
// `pnpm build` puro deixaria a URL do `.env.local` dentro do bundle do
94+
// browser: servidor falando com um banco e cliente com outro, no mesmo
95+
// teste. O `e2e:build` existe para exportar o `.env.e2e` antes do build.
96+
const buildPuro = COMANDOS.some((l) => /^run: pnpm build$/.test(l) || /^pnpm build$/.test(l));
97+
expect(
98+
buildPuro,
99+
"o workflow do e2e builda com o env errado — use `pnpm e2e:build`",
100+
).toBe(false);
101+
expect(packageJson.scripts["e2e:build"]).toBeTruthy();
102+
});
103+
});

0 commit comments

Comments
 (0)