fix(campaign): desbloqueia deploy — gate 'writable' libera coordinator/candidate e FAB ganha data-slot do e2e (C142)
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).
This commit is contained in:
parent
98d683b4e7
commit
a97cd56fce
6 changed files with 97 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
1
docs/changelog/2026-08-23-fix-deploy-c142.md
Normal file
1
docs/changelog/2026-08-23-fix-deploy-c142.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -18,6 +18,7 @@ export const CampaignQuickActionsFab = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
data-slot="campaign-quick-actions-fab"
|
||||
className={cn(
|
||||
'pointer-events-none fixed bottom-4 right-4 z-40 print:hidden',
|
||||
'pb-[max(0px,env(safe-area-inset-bottom))]',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,13 @@ export const requireCampaignPageActor = async (
|
|||
if (gate === 'staff' && !isStaffCampaignRole(user.role)) redirect(denyRedirect)
|
||||
if (gate === 'unrestricted' && !isUnrestrictedCampaignRole(user.role)) redirect(denyRedirect)
|
||||
if (gate === 'noLeader' && user.role === 'leader') redirect(denyRedirect)
|
||||
if (gate === 'writable' && advisorEditingAccess(user) === 'none') redirect(denyRedirect)
|
||||
if (gate === 'writable' && !isStaffCampaignRole(user.role)) redirect(denyRedirect)
|
||||
// C142 — `somente_leitura` applies to advisors only; `advisorEditingAccess`
|
||||
// returns 'none' for every non-advisor role, so gate on the role first or
|
||||
// coordinators/candidates are wrongly redirected away from write pages.
|
||||
if (gate === 'writable' && user.role === 'advisor' && advisorEditingAccess(user) === 'none') {
|
||||
redirect(denyRedirect)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
|
|
|||
81
tests/unit/campaignPageActorGate.unit.spec.ts
Normal file
81
tests/unit/campaignPageActorGate.unit.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
|
@ -118,6 +118,11 @@ describe('CampaignQuickActionsFab (B126)', () => {
|
|||
rerender(<CampaignQuickActionsFab open onOpenChange={vi.fn()} />)
|
||||
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(<CampaignQuickActionsFab open={false} onOpenChange={vi.fn()} />)
|
||||
expect(container.querySelector('[data-slot="campaign-quick-actions-fab"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CampaignQuickActionsOverlay (B126)', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue