diff --git a/AGENTS-infra.md b/AGENTS-infra.md index 493e2201..2349463c 100644 --- a/AGENTS-infra.md +++ b/AGENTS-infra.md @@ -2,7 +2,7 @@ ## Deploy & CI -Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). **CI/PR/deploy live on GitHub Actions (OPS71, 2026-08-19); the ISSUE TRACKER stays on the Forgejo** (`git.solla.dev/fsolla/teqo` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips all keep talking to the Forgejo API; GitHub is only the Actions host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main`. **There is NO main verifier (`ci.yml`) anymore**: publishing is a **manual `workflow_dispatch`** (`.github/workflows/deploy.yml`) with two jobs — `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). The remote Docker deployment is gone; the production container runs the monolith directly. The Docker Compose file is used ONLY for local dev databases. The `run-deploy.yml` (OPS38/OPS46) was deleted in OPS71. There is no `run-deploy.yml` or equivalent in GitHub Actions — deploy is `deploy.yml` only. +Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). **CI/PR/deploy AND the ISSUE TRACKER live on GitHub (OPS71 + OPS76, 2026-08-21)** (`github.com/fsolla/teqo/issues` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips talk to the GitHub API; GitHub is the single host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main`. **There is NO main verifier (`ci.yml`) anymore**: publishing is a **manual `workflow_dispatch`** (`.github/workflows/deploy.yml`) with two jobs — `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). The remote Docker deployment is gone; the production container runs the monolith directly. The Docker Compose file is used ONLY for local dev databases. The `run-deploy.yml` (OPS38/OPS46) was deleted in OPS71. There is no `run-deploy.yml` or equivalent in GitHub Actions — deploy is `deploy.yml` only. **Guarding `pnpm push` (OPS24+):** every PR push goes through `scripts/git-push.mjs`, which runs `ensure-repo-deps` (install + Husky in worktrees — OPS51), then `gate:push` (the pre-push hook), which chains `gate:ci` — a serialized mirror of the CI `checks` job (**without e2e**: OPS72 moved e2e to an explicit skill step) — plus the `docs-guards` (changelog append-only, aggregation sync, conflict markers — OPS63; the rewrite escape stays in CI only). The hook blocks `git push` on failure; escape: `git push --no-verify` (WIP only). There is no YAML `push` gate — the hook is the gate. **CI red on your PR is yours**, even pre-existing or infra — "out of feature scope" is not a stop condition (policy "Dono do PR, dono do CI" in `docs/AGENT-OPS.md`). diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 3b2538cf..788c8058 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,9 +4,7 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- -**OPS46-S1 — hook ESM mapeia next/cache incondicionalmente (2026-08-23):** débito da revisão estrutural do OPS46 (issue #789): `tests/helpers/e2eEsmResolve.mjs` remapeava `next/cache` → `next/cache.js` sem tentar a resolução bare — quando o next publicar um `exports` map, o remap podia mascarar erro de resolução real (fail-loud vira fail-silent). Fix no branch `next/cache`: `await nextResolve(specifier, context)` primeiro; só em `ERR_MODULE_NOT_FOUND` remapeia para `next/cache.js`; qualquer outro erro propaga. Branch `react-server` intocado (injeção de conditions não mascara erro). Spec unit novo `tests/unit/e2eEsmResolve.unit.spec.ts` (3 casos com `nextResolve` mockado: bare-ok sem remap, `ERR_MODULE_NOT_FOUND` remapeia com `context` intacto, erro de outro código propaga). Verificação: `gate:fast` verde (unit 2432), sanity OPS72: `playwright test --list` coleta 202 testes com o hook carregado. Gatilho de reavaliação: bump major do next (fallback vira código morto e pode ser removido). Impl plan: `docs/plans/ops46-s1-hook-esm-bare-first-impl.md`. - -**OPS47 — agent-pool/SKILL.md aponta para plano com política pré-main obsoleta (--base stage + auto-merge) (2026-08-23):** docs-only, zero mudança de comportamento. O `.agents/skills/agent-pool/SKILL.md:18` cita `docs/plans/agent-pool-orchestrator.md` como referência viva de arquitetura ("Arquitetura e elegibilidade"), mas o plano (escrito 2026-07-30, era pré-main) ainda descrevia a política obsoleta: PR `--base stage`, `issue-done-on-stage-merge.yml`, promote `stage→main`, `refresh-stage` — realidade removida em 2026-08-01 (promote/refresh) e 2026-08-12 (migration-lock). **Edição linha-a-linha do plano (dono do concern), frame histórico do pool dormente (OPS65) preservado:** overview e resumo executivo passam a "workers PR direto para main" com nota do promote removido; diagrama mermaid `pull_request closed em stage`→`em main` e `merge em main → issue-done-on-main-merge.yml`; contrato de prompt do worker `gh pr create --base stage`→`--base main` (reflete o vivo: `POOL_BASE_REF='main'` em `scripts/agent-pool.mjs:70` e prompt `--base main` em `scripts/lib/agent-pool-prompt.mjs:40-41`); gargalo de migrations reescrito de "job `migration-lock`" para `countOpenSchemaPrs` (`scripts/lib/agent-pool-forgejo.mjs:83`, com nota da remoção do job em 2026-08-12); referências à skill deletada `model-selection` → `scripts/lib/agent-pool-models.mjs`; `scripts/lib/agent-github.mjs` (inexistente) → `agent-forgejo.mjs`; `.github/workflows/agent-pool.yml` e `gh workflow run agent-pool.yml` marcados como removidos no OPS65 (dispatch falha 404); `startingRef: 'stage'`→`POOL_BASE_REF` (=`'main'`); D7/D8/D1 e rabbit holes alinhados (sem `refresh-stage`, sem "slot ocupado até merge em stage"). **SKILL.md:** resíduo L79 "Antes do promote, use dispatch na branch" → nota histórica (promote removido 2026-08-01, workflow removido OPS65). **Touch-ups de comentário/label (sem mudança de lógica):** `tests/unit/agentPoolState.unit.spec.ts:103` "CI flipped on stage merge"→"on main merge"; `scripts/lib/agent-pool-state.mjs:131` "merge in stage"→"merge in main". Fora de escopo (órfão/histórico): `docs/plans/paradigma-agentes-paralelos.md`; código do pool e workflows (dormentes, não tocados). Impl plan: `docs/plans/agent-pool-skill-md-aponta-para-plano-com-politica-pre-impl.md`. +**OPS73-FOLLOWUP — re-home do S10 (pixel Meta) no GitHub main, origin do workspace homeserver + race do seedUser no e2e full (2026-08-23):** fechamento dos débitos colhidos no OPS73 (#760). Das três fases planejadas, **duas já estavam resolvidas** por eventos posteriores ao plano de intenção (escrito 2026-08-20): (1) o **S10 não precisou de cherry-pick** — o merge de convergência OPS75 (`c723d6ac`, GitHub como fonte única, 2026-08-21) já trouxe o pixel Meta completo para o GitHub main (os 11 arquivos da feature são byte-idênticos à ponta Forgejo `504f6c37`; migração `20260819_213947_add_site_settings_facebook_pixel_id`, e2e `campaignHomePixel`, unit `campaignNewsletterLead`, changelog/plano todos em main); (2) o **origin do workspace do homeserver** (`~/teqo-deploy`) já era `https://github.com/fsolla/teqo.git` — verificado via SSH, e o `deploy-homeserver.sh` re-aponta idempotentemente a cada deploy. O **código real da entrega** foi o resíduo da F3: a race do usuário compartilhado `dev@payloadcms.com` no e2e full estava **parcialmente fixa** no main (o simplify do S10 trouxe `seedTestUser` create-if-missing + helper `adminHeaders` com `pg_advisory_lock(727001)`), mas restavam 3 pontos de risco: o `afterAll` de `admin.e2e.spec.ts` ainda **deletava** o usuário compartilhado no meio da suíte paralela (`cleanupTestUser`), e dois specs roteavam logins REST diretos **sem o advisory lock** (`admin.e2e.spec.ts` ×2 e a função inline `adminHeaders` sem lock em `frontend.e2e.spec.ts`). A entrega removeu o `cleanupTestUser` (chamada + helper morto, zero call sites) e roteou todos os logins restantes pelo helper com lock — um único ponto de serialização para a suite inteira. Deploy do main (que inclui o S10) via `deploy.yml` disparado manualmente: o `verify` full **flakou** com 3 reds + 5 flaky pré-existentes **não relacionados** ao seedUser (timeouts de `locator.check` em `campaignMunicipalities:115`, `campaignPermissionProfile` C142, etc. — classe consolidada na casa de flakes #553), então o deploy precisa de re-dispatch pós-merge. Dívida capturada: **#805** (`E2E-ADMIN-LOGIN-LOCK`, depends #760) — o `beforeEach` UI login do admin (page-based, form) continua sem o lock (resíduo da própria classe que o #760 ataca); correção inline de docs drift em `AGENTS-infra.md` (tracker já migrado para GitHub no OPS76). **OPS46 — infra de teste e2e: coleta local consertada (next/cache) + allocator de município sem colisão no wrap (2026-08-23):** entrega de dois débitos do OPS45 (capture-review-debts). **Fase 1 — e2e local não coleta:** `pnpm exec playwright test --list` falhava em todo spec com `Cannot find module 'node_modules/next/cache'` (e o diagnóstico correto do CI-vs-máquina: não há delta — o CI só roda pelo script `pnpm test:e2e`, que carrega `--conditions=react-server --import=tsx/esm` no `NODE_OPTIONS`; comando bare não tinha nada). Causa: `next@15` não declara `exports` → ESM nativo não resolve `next/cache` (o arquivo é `cache.js`), e `server-only` precisa da condição `react-server`. Fix no runner, zero mudança em produção: hook de resolução ESM vitest-free `tests/helpers/e2eEsmResolve.mjs` (mapeia `next/cache`→`next/cache.js` e injeta `react-server` nas conditions) registrada no processo main via `module.register` e propagada aos workers via `NODE_OPTIONS=--import=e2eEsmLoader.mjs` no `playwright.config.ts` — só quando os flags do script já não estão presentes. Aceite: bare `playwright test --list` coleta 198 testes em 41 arquivos (antes: 0); runs reais bare admin 5/5 e campaign 18/18; `pnpm test:e2e` intocado. **Fase 2 — allocator de município com wrap destrutivo:** a sequência persistente `campaign_fixture_municipality_alloc` nunca é resetada e um ciclo completo de suíte claima ~505 do catálogo de 435 (medido: 432 no int + 73 no e2e) → o wrap acontece em todo ciclo em DB persistente de worktree, e dois runs concorrentes podiam receber o mesmo índice e purgar as linhas vivas um do outro (purge-on-claim). Fix: registry de claims em módulo compartilhado vitest-free `tests/helpers/campaignMunicipalityAllocator.ts` — tabela `campaign_fixture_municipality_claims` (`index` PK, `run_id`, `claimed_at`), claim via `INSERT ON CONFLICT DO NOTHING RETURNING` (conflito = slot vivo → pula; claim stale >2h de run crashado → rouba; tudo ocupado → erro claro de exaustão), release no `cleanup()` dos dois fixtures (int + e2e, só quando claimaram). Guard estático novo no `testMunicipalityAllocatorConventions` (proíbe `nextval`/sequence fora do módulo) + spec int de stress `campaignMunicipalityAllocator.int.spec.ts` (exclusividade viva por 3 wraps, exaustão+recuperação, steal, release). Verificação: tsc/lint/format/cycles verdes (knip: limitação P3 pré-existente de não carregar payload.config, igual no HEAD), unit 2420 + int 819 (92 arquivos) verdes com tabela de claims zerada ao final, e2e local afetado (admin 5/5 + campaign 18/18 bare) verdes; importMap.js restaurado com envs `S3_*` dummy após o `--list` bare regenerá-lo sem elas (classe OPS69 — o guard do #87 cobre no CI). Impl plan: `docs/plans/ops46-infra-teste-e2e-local-allocator-impl.md`. diff --git a/docs/changelog/2026-08-23-ops73-followup.md b/docs/changelog/2026-08-23-ops73-followup.md new file mode 100644 index 00000000..1551ef86 --- /dev/null +++ b/docs/changelog/2026-08-23-ops73-followup.md @@ -0,0 +1 @@ +**OPS73-FOLLOWUP — re-home do S10 (pixel Meta) no GitHub main, origin do workspace homeserver + race do seedUser no e2e full (2026-08-23):** fechamento dos débitos colhidos no OPS73 (#760). Das três fases planejadas, **duas já estavam resolvidas** por eventos posteriores ao plano de intenção (escrito 2026-08-20): (1) o **S10 não precisou de cherry-pick** — o merge de convergência OPS75 (`c723d6ac`, GitHub como fonte única, 2026-08-21) já trouxe o pixel Meta completo para o GitHub main (os 11 arquivos da feature são byte-idênticos à ponta Forgejo `504f6c37`; migração `20260819_213947_add_site_settings_facebook_pixel_id`, e2e `campaignHomePixel`, unit `campaignNewsletterLead`, changelog/plano todos em main); (2) o **origin do workspace do homeserver** (`~/teqo-deploy`) já era `https://github.com/fsolla/teqo.git` — verificado via SSH, e o `deploy-homeserver.sh` re-aponta idempotentemente a cada deploy. O **código real da entrega** foi o resíduo da F3: a race do usuário compartilhado `dev@payloadcms.com` no e2e full estava **parcialmente fixa** no main (o simplify do S10 trouxe `seedTestUser` create-if-missing + helper `adminHeaders` com `pg_advisory_lock(727001)`), mas restavam 3 pontos de risco: o `afterAll` de `admin.e2e.spec.ts` ainda **deletava** o usuário compartilhado no meio da suíte paralela (`cleanupTestUser`), e dois specs roteavam logins REST diretos **sem o advisory lock** (`admin.e2e.spec.ts` ×2 e a função inline `adminHeaders` sem lock em `frontend.e2e.spec.ts`). A entrega removeu o `cleanupTestUser` (chamada + helper morto, zero call sites) e roteou todos os logins restantes pelo helper com lock — um único ponto de serialização para a suite inteira. Deploy do main (que inclui o S10) via `deploy.yml` disparado manualmente: o `verify` full **flakou** com 3 reds + 5 flaky pré-existentes **não relacionados** ao seedUser (timeouts de `locator.check` em `campaignMunicipalities:115`, `campaignPermissionProfile` C142, etc. — classe consolidada na casa de flakes #553), então o deploy precisa de re-dispatch pós-merge. Dívida capturada: **#805** (`E2E-ADMIN-LOGIN-LOCK`, depends #760) — o `beforeEach` UI login do admin (page-based, form) continua sem o lock (resíduo da própria classe que o #760 ataca); correção inline de docs drift em `AGENTS-infra.md` (tracker já migrado para GitHub no OPS76). \ No newline at end of file diff --git a/docs/plans/ops73-followup-rehome-s10-infra-impl.md b/docs/plans/ops73-followup-rehome-s10-infra-impl.md new file mode 100644 index 00000000..f6657e48 --- /dev/null +++ b/docs/plans/ops73-followup-rehome-s10-infra-impl.md @@ -0,0 +1,102 @@ +# Impl: Follow-up pós-OPS73 — re-home S10 no GitHub, origin do workspace homeserver + fix da race de seedUser do e2e + +Status: rascunho +Atualizado em: 2026-08-23 +Issue: #760 +Intenção: docs/plans/ops73-followup-rehome-s10-infra.md +Appetite restante: ~0,4 dia (herdado) + +## Leitura da intenção + +- **Outcome:** (1) S10 (pixel Meta na home de campanha) no GitHub main **e rodando em prod**; (2) `~/teqo-deploy` no homeserver com origin = GitHub; (3) e2e full do GitHub CI estável no `seedTestUser` (sem flake de duplicate email). +- **O que NÃO negociar:** DB de prod intocável (testes/dev nunca apontam pra ele); migration `20260819_213947_add_site_settings_facebook_pixel_id` já registrada — imutável; o usuário compartilhado `dev@payloadcms.com` e a home `/` de campanha com `#meta-pixel-*` são contratos existentes; e2e full do CI continua rodando com 4 workers paralelos. +- **O que reavaliar:** a hipótese central da intenção — "S10 não está no GitHub main" — **está errada**. O merge de convergência OPS75 `c723d6ac` (2026-08-21) trouxe o S10 completo para o main (os 11 arquivos S10 são byte-idênticos entre `504f6c37` e `origin/main`; diff vazio). **Não há cherry-pick a fazer.** F2 também já está feito (`origin = https://github.com/fsolla/teqo.git` verificado no homeserver). O único **código real** restante é o resíduo de F3: logins sem advisory lock e o `cleanupTestUser()` deletando o usuário compartilhado no meio da suíte paralela. + +## Abordagem recomendada + +```mermaid +flowchart LR + subgraph F1["F1 — deploy + verificação (sem cherry-pick)"] + A1[Deploy manual em andamento no GH Actions
verify full → deploy] --> A2[Pós-deploy: revision nova no teqo-1313
+ #meta-pixel-* na home / com facebookPixelId setado] + end + subgraph F2["F2 — verificação (já feito)"] + B1[git -C ~/teqo-deploy remote get-url origin = GitHub] + end + subgraph F3["F3 — resíduos da race do usuário compartilhado"] + C1[admin.e2e.spec.ts: remover afterAll cleanupTestUser
+ remover helper cleanupTestUser morto] + C2[admin.e2e.spec.ts:50,152 + frontend.e2e.spec.ts:613
roteados pelo helper adminHeaders com advisory lock] + end + A2 --> G[Gate:fast + changelog + push] + B1 --> G + C1 --> C2 --> G +``` + +**Opções consideradas:** A | B | C (duas decisões independentes em F3) + +**Decisão 1 — destino do `cleanupTestUser()` (admin.e2e.spec.ts:18-20):** +**Recomendação:** **B** — remover a chamada no `afterAll` **e** o helper `cleanupTestUser` de `tests/helpers/seedUser.ts`. O `seedTestUser` já é create-if-missing com credenciais constantes: o delete no fim da suíte não serve a nada e abre a janela "user vanished" + invalidação de tokens para workers paralelos ainda rodando. Remover só a chamada (opção A) deixaria um export morto — knip roda com `"exports": "error"` (knip.jsonc) e o projeto inclui `tests/**/*.ts`, então o gate de CI quebraria; código morto também viola o princípio de simplify. +**Rejeitadas:** A — remover só a chamada e manter o helper (export morto → knip `exports: error`); C — manter o `afterAll` mas neutralizar o delete (código morto idêntico, pior: ritual enganoso). + +**Decisão 2 — rotear os logins diretos pelo lock:** +**Recomendação:** **A** — substituir os logins diretos (admin.e2e.spec.ts:50 e :152) por `adminHeaders(request, baseURL)` do helper `tests/helpers/adminApi.ts`, e no frontend.e2e.spec.ts **apagar a função local `adminHeaders` (:613-620)** e importar o helper. O advisory lock `pg_advisory_lock(727001)` já é o ponto único de serialização da sessão read-modify-write do Payload — um lock por login, um por suite inteira; rotear os call sites existentes (608, 810, 968, 1001, 1082, 1135, 1199, 1289, 1347 no frontend) custa zero mudança de comportamento além da serialização. O `campaignHomePixel.e2e.spec.ts` e o `campaignNewsletter.e2e.spec.ts` já usam o helper. +**Rejeitadas:** B — adicionar lock ad hoc em cada login inline (duplica a mecânica do lock em N pontos; o helper já existe — DRY, viola depth check de pass-through); C — serializar os arquivos com `test.describe.configure({ mode: 'serial' })` (mata o paralelismo de 4 workers, infla o wall-clock do full, e trata o sintoma de perda de sessão com perda de throughput). + +**F1/F2:** nenhuma decisão de engenharia — monitorar o deploy em andamento e verificar. Se o deploy atual não cobrir o main com S10, re-disparar `deploy.yml` manual (`workflow_dispatch`). + +### Componentes / mudanças + +- **`tests/e2e/admin.e2e.spec.ts`**: remove `cleanupTestUser` do import (:2) e do `afterAll` (:18-20); troca os dois logins diretos (:50-55 e :152-156) por `adminHeaders(request, baseURL)` do helper. +- **`tests/e2e/frontend.e2e.spec.ts`**: apaga a definição local `adminHeaders` (:613-620, que sobrescreve o helper no arquivo) e importa `adminHeaders` de `../helpers/adminApi`; todos os ~9 call sites passam a usar o helper — a assinatura exige `baseURL` (já definido no arquivo), então cada chamada passa `request, baseURL`. +- **`tests/helpers/seedUser.ts`**: remove o export `cleanupTestUser` (zero call sites após a mudança acima). +- **`tests/helpers/adminApi.ts`**: sem mudança — dono do lock (1 lock `727001` serializa a suíte inteira). +- **Migration:** sem migration (só testes/e2e; schema intocado). +- **Access / Consent:** N/A — nada de prod, PII ou Consent. +- **UI:** N/A. +- **Changelog:** `docs/changelog/2026-08-23-ops73-followup.md` + `pnpm changelog:build`. + +### Dados → forma (se aplicável) + +N/A — sem mudança de dados; as mudanças são exclusivamente em specs e helper de teste. + +## Fases verificáveis + +1. **F1 — verificar deploy do S10** (dominante no appetite): monitorar o `workflow_dispatch` em andamento (verify full → deploy). Pós-deploy no homeserver: conferir revision nova no container `teqo-1313` e `#meta-pixel-*` presente na home `/` com `SiteSettings.facebookPixelId` setado. Se o deploy atual não cobrir, disparar `deploy.yml` manual. Verificação via SSH/`docker` no homeserver (leitura apenas). +2. **F2 — verificação barata**: `git -C ~/teqo-deploy remote get-url origin` → deve devolver `https://github.com/fsolla/teqo.git`. Sem code change. +3. **F3 — código + gates**: editar os 3 arquivos de teste conforme Componentes/mudanças; `pnpm gate:fast` (lint + typecheck + unit; knip entra no cascade do CI); e2e afetado: `admin`, `frontend`, `campaignHomePixel`, `campaignNewsletter` (o `deploy.yml` `verify` roda a full e é o juiz final). + +## Rabbit holes / Não escopo (engenharia) + +- **Não** reabrir o diagnóstico do admin (OPS73 fechado). +- **Não** re-homar S8/S9 (já em main via PR #751). +- **Não** reescrever `seedTestUser`/`assertTestDatabase`/chave do lock nem mudar `dev@payloadcms.com`. +- **Não** editar a migration `20260819_213947_...` nem schema. +- **Não** reduzir workers (4→1) nem serializar specs para "consertar" a race. +- **Não** mexer no retry 403→relogin do `campaignHomePixel.e2e.spec.ts:41`: com todos os logins no lock ele fica inerte — removê-lo é polish barato, deferível, sem gatilho de risco. +- Guard #87 (importMap sem envs S3) é Issue própria. +- F1/F2 não exigem cherry-pick nem arquivos do repo. + +## Riscos e mitigação + +- **Deploy em andamento falha no verify (flake)**: o próprio resíduo de F3 pode flakear o full em prod mode → mitigação: F3 reduz essa flakiness; após merge, re-disparar `deploy.yml` se necessário. +- **Pixel ausente em prod mesmo com deploy ok**: `SiteSettings.facebookPixelId` vazio no admin de prod faz a home renderizar sem `#meta-pixel-*` → mitigação: passo explícito de verificação em F1; se vazio, o ID real precisa ser configurado no admin (é dado de produto — perguntar ao humano, nunca inventar ID). +- **Shadowing no frontend.e2e.spec.ts**: `adminHeaders(request)` sem `baseURL` em :608 quebra na assinatura do helper → mitigação: passar `request, baseURL` em todos os call sites; o typecheck pega antes do push. +- **Knip pega resíduo inesperado ao remover o helper**: gates locais (`gate:fast` + knip) antes do push. +- **Usuário persistente entre runs após remover o cleanup**: aceito — credenciais constantes, create-if-missing, é o estado pós-seed atual. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto (S10 em main + prod verificado; origin homeserver = GitHub; e2e full sem flake de duplicate email) +- [ ] Invariantes AGENTS/engineering-standards (testes/dev nunca tocam prod; migration imutável; sem collection/Consent/URL nova) +- [ ] Testes de domínio previstos onde write paths mudam: sem write path de app — e2e afetados (`admin`, `frontend`, `campaignHomePixel`, `campaignNewsletter`) verdes no full +- [ ] Changelog `docs/changelog/2026-08-23-ops73-followup.md` + `pnpm changelog:build` +- [ ] `pnpm gate:fast` verde (lint + typecheck + unit) e knip sem export morto + +--- + +**Self-score decision-quality: 5/5** (gate ≥4) + +1. Decisões caras têm rejeitadas? **Sim** — as duas decisões reais (destino do `cleanupTestUser`; roteamento dos logins) têm A/B/C documentado. +2. Abordagem cabe no appetite? **Sim** — F1/F2 são verificação/monitoramento; F3 é edição pontual em 3 arquivos de teste; ~0,4 dia cabe. +3. Rabbit holes nomeados? **Sim** — seção própria com 7 itens (incl. workers, retry 403, migration, S8/S9). +4. Depth check: reusa shells/helpers? **Sim** — roteia tudo pelo `adminHeaders`/advisory lock existente; não cria helper novo nem duplica lock. +5. Intenção permanece satisfeita? **Sim** — aceite de produto intacto; a divergência é só a hipótese de direção ("S10 fora do main"), corrigida e documentada em Leitura da intenção. diff --git a/tests/e2e/admin.e2e.spec.ts b/tests/e2e/admin.e2e.spec.ts index eeb06e7b..9ce1a1e1 100644 --- a/tests/e2e/admin.e2e.spec.ts +++ b/tests/e2e/admin.e2e.spec.ts @@ -1,5 +1,6 @@ +import { adminHeaders } from '../helpers/adminApi' import { login } from '../helpers/login' -import { cleanupTestUser, seedTestUser, testUser } from '../helpers/seedUser' +import { seedTestUser, testUser } from '../helpers/seedUser' import { instagramStubUrlFor } from '../helpers/socialStub' import { expect, test } from './fixtures/e2eTest' @@ -15,10 +16,6 @@ test.describe('Admin Panel', () => { await login({ page, serverURL: baseURL, user: testUser }) }) - test.afterAll(async () => { - await cleanupTestUser() - }) - test('can navigate to dashboard', async ({ page }) => { await page.goto(`${baseURL}/admin`) await expect(page).toHaveURL(`${baseURL}/admin`) @@ -47,12 +44,7 @@ test.describe('Admin Panel', () => { // Seed the social-feed global with an Instagram snapshot so the exclusion // picker (S3) has posts to show. The snapshot is plain data — the picker // lists it and writes `excludedItems` (the same mechanism YouTube uses). - const login = await request.post(`${baseURL}/api/users/login`, { - data: { email: testUser.email, password: testUser.password }, - }) - expect(login.ok()).toBeTruthy() - const { token } = await login.json() - const headers = { cookie: `payload-token=${token}` } + const headers = await adminHeaders(request, baseURL) const snapshot = { username: 'depjorgesolla', @@ -149,12 +141,7 @@ test.describe('Admin Panel', () => { } await setStub('ok') - const login = await request.post(`${baseURL}/api/users/login`, { - data: { email: testUser.email, password: testUser.password }, - }) - expect(login.ok()).toBeTruthy() - const { token } = await login.json() - const headers = { cookie: `payload-token=${token}` } + const headers = await adminHeaders(request, baseURL) const seed = await request.post(`${baseURL}/api/globals/social-feed-settings`, { headers, diff --git a/tests/e2e/campaignHomePixel.e2e.spec.ts b/tests/e2e/campaignHomePixel.e2e.spec.ts index da768970..9fb983e9 100644 --- a/tests/e2e/campaignHomePixel.e2e.spec.ts +++ b/tests/e2e/campaignHomePixel.e2e.spec.ts @@ -33,11 +33,10 @@ test.describe('Campaign home Meta pixel', () => { headers: Record, pixelId: string | null, ) => { - // Insurance against the same-user concurrent-login session race with - // specs that do NOT route their login through the advisory-locked - // `adminHeaders` helper (e.g. the frontend spec's inline logins): a 403 - // means the session row was lost to another worker — re-login mints a - // fresh one. The locked helper already serializes the specs that use it. + // Insurance against the same-user concurrent-login session race during + // retries of this very spec's own non-serialized write: a 403 means the + // session row was lost to another worker — re-login mints a fresh one. + // The locked helper already serializes the specs that use it. for (let attempt = 0; attempt < 2; attempt += 1) { const response = await request.post(`${baseURL}/api/globals/site-settings`, { headers, diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts index b0e7fd27..72555176 100644 --- a/tests/e2e/frontend.e2e.spec.ts +++ b/tests/e2e/frontend.e2e.spec.ts @@ -1,6 +1,7 @@ import type { APIRequestContext, Locator, Page } from '@playwright/test' -import { seedTestUser, testUser } from '../helpers/seedUser' +import { adminHeaders } from '../helpers/adminApi' +import { seedTestUser } from '../helpers/seedUser' import { instagramStubUrlFor, youtubeStubUrlFor } from '../helpers/socialStub' import { expect, test } from './fixtures/e2eTest' @@ -605,20 +606,11 @@ test.describe('Campaign home content section', () => { // Start every run with the social feed unconfigured so the empty-state // test stays deterministic even when a previous S2 run left settings or a // persisted snapshot behind (same reasoning as the `posts` bust below). - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await resetSocialFeedSettings(request, headers) await bustSocialFeed(request) }) - const adminHeaders = async (request: APIRequestContext): Promise> => { - const login = await request.post(`${baseURL}/api/users/login`, { - data: { email: testUser.email, password: testUser.password }, - }) - expect(login.ok()).toBeTruthy() - const { token } = await login.json() - return { cookie: `payload-token=${token}` } - } - const createTag = async ( request: APIRequestContext, headers: Record, @@ -807,7 +799,7 @@ test.describe('Campaign home content section', () => { request, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) const visibleTag = await createTag(request, headers, categoryName, categorySlug) const hiddenTag = await createTag(request, headers, hiddenTagName, hiddenTagSlug, true) @@ -965,7 +957,7 @@ test.describe('Campaign home content section', () => { await page.goto('about:blank') await expect(page).toHaveURL(/about:blank/) } finally { - const headers2 = await adminHeaders(request).catch(() => undefined) + const headers2 = await adminHeaders(request, baseURL).catch(() => undefined) if (headers2) { for (const id of createdPosts) { await request @@ -998,7 +990,7 @@ test.describe('Campaign home content section', () => { request, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setYouTubeStubState(request, 'ok') const mixTag = await createTag(request, headers, `E2e Mix ${runSuffix}`, `e2e-mix-${runSuffix}`) @@ -1079,7 +1071,7 @@ test.describe('Campaign home content section', () => { request, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setYouTubeStubState(request, 'fail') const fallbackTag = await createTag( @@ -1132,7 +1124,7 @@ test.describe('Campaign home content section', () => { test('keeps the last snapshot while the API is down', async ({ page, request }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setYouTubeStubState(request, 'ok') const baseSettings = { enabled: true, @@ -1196,7 +1188,7 @@ test.describe('Campaign home content section', () => { request, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setYouTubeStubState(request, 'ok') await setInstagramStubState(request, 'ok') @@ -1286,7 +1278,7 @@ test.describe('Campaign home content section', () => { request, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setInstagramStubState(request, 'fail') const fallbackTag = await createTag( @@ -1344,7 +1336,7 @@ test.describe('Campaign home content section', () => { test('keeps the last Instagram snapshot while the API is down', async ({ page, request }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setInstagramStubState(request, 'ok') const baseSettings = { enabled: true, @@ -1415,7 +1407,7 @@ test.describe('Campaign home content section', () => { request, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setYouTubeStubState(request, 'ok') await setInstagramStubState(request, 'ok') @@ -1480,7 +1472,7 @@ test.describe('Campaign home content section', () => { context, }) => { await seedTestUser() - const headers = await adminHeaders(request) + const headers = await adminHeaders(request, baseURL) await setYouTubeStubState(request, 'ok') await setInstagramStubState(request, 'ok') diff --git a/tests/helpers/seedUser.ts b/tests/helpers/seedUser.ts index e251aedd..f3059de5 100644 --- a/tests/helpers/seedUser.ts +++ b/tests/helpers/seedUser.ts @@ -35,20 +35,3 @@ export async function seedTestUser(): Promise { // A parallel worker already seeded the shared user; nothing to reset. } } - -/** - * Cleans up test user after tests - */ -export async function cleanupTestUser(): Promise { - assertTestDatabase(process.env.DATABASE_URL) - const payload = await getPayload({ config }) - - await payload.delete({ - collection: 'users', - where: { - email: { - equals: testUser.email, - }, - }, - }) -}