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,
- },
- },
- })
-}