From ac028ca603e4c8741e4e9d9ca4de4b80456340ae Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sun, 23 Aug 2026 16:12:10 -0300 Subject: [PATCH] =?UTF-8?q?fix(campaign):=20desbloqueia=20deploy=20?= =?UTF-8?q?=E2=80=94=20gate=20'writable'=20libera=20coordinator/candidate?= =?UTF-8?q?=20e=20FAB=20ganha=20data-slot=20do=20e2e=20(C142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dois bugs do C142 que o e2e full do verify do deploy pegou (o PR rodou só o blast radius): - requireCampaignPageActor gate 'writable' checava advisorEditingAccess(user)==='none', que retorna 'none' para QUALQUER papel não-advisor — barrava coordinator/candidate de páginas de escrita (municipios/[slug]/editar redirecionava o coordinator pro detalhe). - CampaignQuickActionsFab não renderizava data-slot='campaign-quick-actions-fab', o locator do e2e campaignPermissionProfile. Regressões pino: campaignPageActorGate.unit.spec.ts (5 casos) + assert de data-slot no spec do FAB. Verificado: tsc/lint/format/cycles, unit 2435, int 819, e2e local dos specs afetados (os 3 casos que falhavam no deploy passam). --- docs/CHANGELOG-AGENTS.md | 2 + docs/changelog/2026-08-23-fix-deploy-c142.md | 1 + .../shell/CampaignQuickActionsFab.tsx | 1 + src/utilities/campaignPageActor.ts | 8 +- tests/unit/campaignPageActorGate.unit.spec.ts | 81 +++++++++++++++++++ .../campaignQuickActionsDrawer.unit.spec.tsx | 5 ++ 6 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/2026-08-23-fix-deploy-c142.md create mode 100644 tests/unit/campaignPageActorGate.unit.spec.ts diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 8b6dde21..58d8f08d 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**FIX — desbloqueio do deploy (e2e full vermelho, 2026-08-23):** dois bugs do C142 (merge em main no 21-08) que só o e2e **full** do `verify` pegou — o PR do C142 rodou só o blast radius selecionado e passou. **(1) `gate: 'writable'` barrava coordinator/candidate:** `requireCampaignPageActor` checava `advisorEditingAccess(user) === 'none'`, mas esse helper retorna `'none'` para **qualquer** papel não-advisor — inclusive coordinator e candidate, que têm escrita plena. Resultado: `municipios/[slug]/editar` redirecionava o coordinator para a página de detalhe e o e2e `campaignMunicipalities` (coordinator assigns an advisor) estourava timeout. Fix: o gate exige staff primeiro (`isStaffCampaignRole`) e só então aplica a regra `somente_leitura` **para advisors** (`user.role === 'advisor' && advisorEditingAccess(user) === 'none'`). **(2) FAB sem `data-slot`:** o e2e `campaignPermissionProfile` loca `[data-slot="campaign-quick-actions-fab"]`, mas `CampaignQuickActionsFab` nunca renderizou esse atributo — os asserts `toBeVisible()` dos perfis `carteira`/`tudo` falhavam com "element(s) not found" (o `toBeHidden()` do perfil `somente_leitura` passava porque ausência conta como hidden). Fix: `data-slot="campaign-quick-actions-fab"` no div raiz do FAB. Regressões pino: `tests/unit/campaignPageActorGate.unit.spec.ts` (5 casos do gate writable, incluindo coordinator/candidate passando e advisor somente_leitura bloqueado) + assert do data-slot no `campaignQuickActionsDrawer.unit.spec.tsx`. Verificação: tsc/lint/format/cycles verdes (knip: limitação P3 pré-existente), unit 2435 + int 819 verdes, e2e local dos dois specs afetados verdes para os 3 casos que falhavam no deploy (`campaignMunicipalities` coordinator-assigns-advisor e `campaignPermissionProfile` carteira/tudo FAB); os dois remanescentes locais (key-warning dev-only do omnibox no `campaignPermissionProfile:69` e `campaignMunicipalities:638`) passam no build de produção do CI — não são bloqueadores. + **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). **OPS78 — default de launch do worktree opencode → DeepSeek V4 Flash max via Vercel AI Gateway (2026-08-23):** o launch de `pnpm worktree next/plan/new` abria o opencode com o preset `deepseek/deepseek-v4-flash` (rota direta ao provedor). A política mudou: a sessão de launch deve rotear pelo **Vercel AI Gateway** (cache/estabilidade/failover) e expor a variante de raciocínio mais forte (`max`). Mudança config-only (mecanismo OPS26 "trocar modelo = editar a constante"): `OPENCODE_PRESET_MODEL` em `scripts/lib/worktree.mjs` → `vercel/deepseek/deepseek-v4-flash`; o repo `opencode.json` ganhou o override `provider.vercel.models["deepseek/deepseek-v4-flash"].variants` com low/high/max (cada uma com `reasoningEffort`) — o catálogo built-in do opencode não expõe as variantes para esse modelo, e a sobrescreve as adiciona sem quebrar o routing (fix validado ao vivo: `opencode debug config` + `opencode run --model vercel/deepseek/deepseek-v4-flash --variant max`). Fato verificado no source do opencode 1.18: o TUI não tem flag `--variant` (só `opencode run`) e `--model` é estritamente `provider/model`, então a variante é selecionável no TUI e sticky por model (state do opencode) — a verificação do aceite é a linha de launch emitida pelo preset. 8 literais + pin da constante em `tests/unit/worktree.unit.spec.ts` atualizados; textos em `scripts/worktree.mjs` (docblock/help), `.agents/shell/worktree.sh` e `worktree-next-issue/SKILL.md` sincronizados. Anti-goals preservados: cardápio de execução de Issues intocado (`work-issue.md`/`plan-issue.md` + pin `opencodeCommands.unit.spec.ts` seguem com o provider direto), config global do humano intocada, docs históricos congelados. Verificação: `pnpm gate:fast` verde; `gate:push` na entrega; PR em main com auto-merge. diff --git a/docs/changelog/2026-08-23-fix-deploy-c142.md b/docs/changelog/2026-08-23-fix-deploy-c142.md new file mode 100644 index 00000000..e2adc9fd --- /dev/null +++ b/docs/changelog/2026-08-23-fix-deploy-c142.md @@ -0,0 +1 @@ +**FIX — desbloqueio do deploy (e2e full vermelho, 2026-08-23):** dois bugs do C142 (merge em main no 21-08) que só o e2e **full** do `verify` pegou — o PR do C142 rodou só o blast radius selecionado e passou. **(1) `gate: 'writable'` barrava coordinator/candidate:** `requireCampaignPageActor` checava `advisorEditingAccess(user) === 'none'`, mas esse helper retorna `'none'` para **qualquer** papel não-advisor — inclusive coordinator e candidate, que têm escrita plena. Resultado: `municipios/[slug]/editar` redirecionava o coordinator para a página de detalhe e o e2e `campaignMunicipalities` (coordinator assigns an advisor) estourava timeout. Fix: o gate exige staff primeiro (`isStaffCampaignRole`) e só então aplica a regra `somente_leitura` **para advisors** (`user.role === 'advisor' && advisorEditingAccess(user) === 'none'`). **(2) FAB sem `data-slot`:** o e2e `campaignPermissionProfile` loca `[data-slot="campaign-quick-actions-fab"]`, mas `CampaignQuickActionsFab` nunca renderizou esse atributo — os asserts `toBeVisible()` dos perfis `carteira`/`tudo` falhavam com "element(s) not found" (o `toBeHidden()` do perfil `somente_leitura` passava porque ausência conta como hidden). Fix: `data-slot="campaign-quick-actions-fab"` no div raiz do FAB. Regressões pino: `tests/unit/campaignPageActorGate.unit.spec.ts` (5 casos do gate writable, incluindo coordinator/candidate passando e advisor somente_leitura bloqueado) + assert do data-slot no `campaignQuickActionsDrawer.unit.spec.tsx`. Verificação: tsc/lint/format/cycles verdes (knip: limitação P3 pré-existente), unit 2435 + int 819 verdes, e2e local dos dois specs afetados verdes para os 3 casos que falhavam no deploy (`campaignMunicipalities` coordinator-assigns-advisor e `campaignPermissionProfile` carteira/tudo FAB); os dois remanescentes locais (key-warning dev-only do omnibox no `campaignPermissionProfile:69` e `campaignMunicipalities:638`) passam no build de produção do CI — não são bloqueadores. diff --git a/src/components/campaign/shell/CampaignQuickActionsFab.tsx b/src/components/campaign/shell/CampaignQuickActionsFab.tsx index f163ba14..1a4f2c96 100644 --- a/src/components/campaign/shell/CampaignQuickActionsFab.tsx +++ b/src/components/campaign/shell/CampaignQuickActionsFab.tsx @@ -18,6 +18,7 @@ export const CampaignQuickActionsFab = ({ return (
({ + redirect: vi.fn(), + getCampaignUser: vi.fn(), +})) + +vi.mock('next/navigation', () => ({ + redirect: mocks.redirect, +})) + +vi.mock('@/utilities/campaignAuth', () => ({ + getCampaignUser: mocks.getCampaignUser, +})) + +vi.mock('@/utilities/access/shared', () => ({ + advisorEditingAccess: (user: { role: string; visibility?: string; editing?: string }) => { + if (user.role !== 'advisor') return 'none' + if (user.editing === 'somente_leitura') return 'none' + return 'carteira' + }, +})) + +import { requireCampaignPageActor } from '@/utilities/campaignPageActor' + +type Actor = { + role: 'coordinator' | 'candidate' | 'advisor' | 'leader' + visibility?: 'carteira' | 'tudo' + editing?: 'carteira' | 'tudo' | 'somente_leitura' + email: string +} + +const actor = ( + role: Actor['role'], + profile?: { visibility?: Actor['visibility']; editing?: Actor['editing'] }, +): Actor => ({ + role, + email: `${role}@example.com`, + ...profile, +}) + +describe('requireCampaignPageActor — gate: writable (C142)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('lets a coordinator through the writable gate (C142 regression: advisorEditingAccess returns "none" for non-advisors)', async () => { + mocks.getCampaignUser.mockResolvedValue(actor('coordinator')) + const user = await requireCampaignPageActor({ gate: 'writable' }) + expect(user.role).toBe('coordinator') + expect(mocks.redirect).not.toHaveBeenCalled() + }) + + it('lets a candidate through the writable gate', async () => { + mocks.getCampaignUser.mockResolvedValue(actor('candidate')) + const user = await requireCampaignPageActor({ gate: 'writable' }) + expect(user.role).toBe('candidate') + expect(mocks.redirect).not.toHaveBeenCalled() + }) + + it('redirects an advisor with Edição somente_leitura away from write pages', async () => { + mocks.getCampaignUser.mockResolvedValue(actor('advisor', { editing: 'somente_leitura' })) + await requireCampaignPageActor({ gate: 'writable', redirectTo: '/campanha' }) + expect(mocks.redirect).toHaveBeenCalledWith('/campanha') + }) + + it('lets an advisor with carteira editing through the writable gate', async () => { + mocks.getCampaignUser.mockResolvedValue(actor('advisor', { editing: 'carteira' })) + const user = await requireCampaignPageActor({ gate: 'writable' }) + expect(user.role).toBe('advisor') + expect(mocks.redirect).not.toHaveBeenCalled() + }) + + it('blocks a leader (non-staff) on the writable gate', async () => { + mocks.getCampaignUser.mockResolvedValue(actor('leader')) + await requireCampaignPageActor({ gate: 'writable' }) + expect(mocks.redirect).toHaveBeenCalled() + }) +}) diff --git a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx index 4e31a38f..6fb82d0e 100644 --- a/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx +++ b/tests/unit/campaignQuickActionsDrawer.unit.spec.tsx @@ -118,6 +118,11 @@ describe('CampaignQuickActionsFab (B126)', () => { rerender() expect(screen.queryByRole('button', { name: 'Ações rápidas' })).toBeNull() }) + + it('carries the data-slot the e2e permission-profile spec locates (C142)', () => { + const { container } = render() + expect(container.querySelector('[data-slot="campaign-quick-actions-fab"]')).toBeTruthy() + }) }) describe('CampaignQuickActionsOverlay (B126)', () => {