Merge pull request #138 from fsolla/cursor/pool-i129-7bc105bb-7d33

B102 — Crash — busca no bottom drawer (focus / digitação)
This commit is contained in:
Francisco Solla 2026-08-01 15:33:04 -03:00 committed by GitHub
commit de9377de85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 129 additions and 12 deletions

View file

@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
---
**Recently resolved (2026-08-01):** **B102** (Issue #129) — crash na busca do bottom drawer (focus/digitação): `TooltipProvider` do layout `(app)` envolvia só `{children}` e o drawer (irmão do scrollport) renderizava hits `priority:alta` via `CampaignHoverTooltip` sem provider. Fix: provider envolve `CampaignAppScrollChrome`; pins unit focus→suggest e type→search. Plano: [`crash-busca-bottom-drawer.md`](docs/plans/crash-busca-bottom-drawer.md).
**Recently resolved (2026-08-01):** **Pool pin `fast=false`** — `resolvePoolModel` agora envia `fast=false` em todo spawn cuja família Cloud anuncia o param (`composer-2.5`, `grok-4.5`, …). Antes o supervisor pedia só `{ id: "composer-2.5" }`; a API Create-Agent resolve o default da variante (`fast=true`) e o usage dashboard mostrava `composer-2.5-fast` em todos os workers do pool. Skills `model-selection` / `agent-pool` + plano do orquestrador atualizados.
**Recently resolved (2026-08-01):** **C8 F1–F2** (Issue #37) — freshness audit confirmou entrega em código desde 2026-07-19: locks advisory em batch (`acquireTextAdvisoryLocks`), bulk import com `.returning()` + leituras drizzle, overview derivando `total` de `totalDocs`, migration `pg_trgm`, guard `contactSearchQuery` (mín. 2 chars). Plano [`escala-dry-pos-c6.md`](docs/plans/escala-dry-pos-c6.md) fechado; follow-ups de filtros/forms permanecem no C9.

View file

@ -1,14 +1,20 @@
# Crash — busca no bottom drawer (focus / digitação)
Status: registrado
Status: done
Atualizado em: 2026-08-01
Issue: #129 (file-miss #133)
Issue: #129 (file-miss plan #133 · delivery #135)
Priority: P0
Model: cursor-grok-4.5-medium
Impeccable: B — `CampaignQuickActionsDrawer` + `CampaignGlobalSearchBody`
Appetite: ~0,5–1d eng; root-cause + fix + pin focus/type; sem migration
Responsável: —
## Freshness audit (2026-08-01)
- Arquivos citados existem; assinaturas de `useHomeSearch` / `useHomeSearchResultsState` / drawer intactas.
- Sem migration; deps soft B91/B100 done.
- **Root cause confirmada (repro unit):** `TooltipProvider` no layout `(app)` envolvia só `{children}` dentro de `CampaignAppScrollChrome`. O drawer monta como **irmão** do scrollport → fora do provider. Focus → `mode:suggest` → hits `priority:'alta'` → `MunicipalityPriorityIndicator` → `CampaignHoverTooltip` → throw ``Tooltip` must be used within `TooltipProvider`` → página cai. Início não crasha porque o drawer não monta lá e a busca vive sob o provider dos children. Digitar com hits prioritários crashava pelo mesmo motivo; o pin B100 só mudava query com payload vazio (sem flag).
## Design (Impeccable)
Âncoras: `PRODUCT.md` (Feel the action) · B91/B100 drawer search · tema `campaign`.
@ -66,10 +72,11 @@ Diagnóstico preliminar (código em `main`):
- **Fix mínimo no caminho focus→suggest→render; não redesenhar snap.** Geometria/gesto → **B105**. **Rejeitado:** reescrever busca só para o drawer.
- **File-miss obrigatório** com causa + PR culpado quando conhecido. **Rejeitado:** só Issue de feature sem harvest.
- **i18n:** ids existentes; sem copy nova.
- **Fix (as-built):** elevar `TooltipProvider` no layout `(app)` para envolver `CampaignAppScrollChrome` inteiro (scroll + drawer). Harness unit espelha isso.
## Questões em aberto
- **Causa raiz exata antes do claim?** **Opções:** A) fechar no `work-issue` com repro primeiro | B) bipartir plan/exec. **Recomendação:** **A** — appetite cabe; hypothese já estreita. _(assumido)_
- _(nenhuma — root cause fechada no repro unit; fix as-built acima)_
## Abordagem proposta

View file

@ -86,9 +86,16 @@ export default async function CampaignAppLayout({ children }: { children: React.
<CampaignNotificationBellSlot user={user} />
</div>
</header>
<CampaignAppScrollChrome role={user.role}>
<TooltipProvider delayDuration={300}>{children}</TooltipProvider>
</CampaignAppScrollChrome>
{/*
Provider must wrap CampaignAppScrollChrome, not only page children:
the mobile quick-actions drawer (B91/B100) mounts as a sibling of
the scrollport and renders search hits with CampaignHoverTooltip
(priority flag). Nested only around {children} left focus→suggest
without a provider and crashed the page (B102).
*/}
<TooltipProvider delayDuration={300}>
<CampaignAppScrollChrome role={user.role}>{children}</CampaignAppScrollChrome>
</TooltipProvider>
<Toaster position="top-center" />
<InstallPwaToast />
{biometricEnrollment ? <BiometricEnrollmentToast {...biometricEnrollment} /> : null}

View file

@ -10,6 +10,8 @@ import {
CampaignQuickActionsSnapProvider,
useCampaignQuickActionsSnap,
} from '@/components/campaign/shell/CampaignQuickActionsSnapContext'
import { TooltipProvider } from '@/components/ui/tooltip'
import type { HomeSearchSuccessResponse } from '@/lib/campaignHomeSearchHits'
import { postCampaignJson } from '@/lib/campaignJsonRequest'
import {
QUICK_ACTIONS_SCROLL_COLLAPSE_THRESHOLD_PX,
@ -26,9 +28,9 @@ vi.mock('next/navigation', () => ({
usePathname: () => '/campanha/municipios/foo',
}))
const idleSuggestPayload = {
status: 'success' as const,
resultKind: 'suggest' as const,
const idleSuggestPayload: HomeSearchSuccessResponse = {
status: 'success',
resultKind: 'suggest',
municipalities: [],
territories: [],
advisors: [],
@ -38,6 +40,26 @@ const idleSuggestPayload = {
demands: [],
}
/** Priority-alta hit — mounts MunicipalityPriorityIndicator → CampaignHoverTooltip. */
const suggestWithPriorityPayload: HomeSearchSuccessResponse = {
...idleSuggestPayload,
municipalities: [
{
kind: 'municipality',
slug: 'cairu',
name: 'Cairu',
region: 'Baixo Sul',
priority: 'alta',
votePosition2022: null,
},
],
}
const searchWithPriorityPayload: HomeSearchSuccessResponse = {
...suggestWithPriorityPayload,
resultKind: 'search',
}
const matchMediaMock = vi.fn()
beforeEach(() => {
@ -59,11 +81,17 @@ afterEach(() => {
vi.unstubAllGlobals()
})
/**
* Mirrors campaign (app) layout: TooltipProvider wraps chrome that includes the
* drawer (B102). Nested only around page children left focus→suggest crashing.
*/
const renderQuickActionsChrome = (ui: ReactNode) =>
render(
<CampaignQuickActionsSnapProvider>
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
</CampaignQuickActionsSnapProvider>,
<TooltipProvider delayDuration={300}>
<CampaignQuickActionsSnapProvider>
<CampaignGlobalSearchProvider>{ui}</CampaignGlobalSearchProvider>
</CampaignQuickActionsSnapProvider>
</TooltipProvider>,
)
const SnapReadout = () => {
@ -134,6 +162,60 @@ describe('CampaignQuickActionsDrawer (B100)', () => {
})
})
it('focus posts suggest and renders priority hits without crashing (B102)', async () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,
payload: suggestWithPriorityPayload,
})
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
fireEvent.focus(screen.getByLabelText('Buscar na campanha'))
await waitFor(() => {
expect(postCampaignJson).toHaveBeenCalledWith(
'/campanha/home-search',
{ mode: 'suggest' },
expect.any(AbortSignal),
)
})
await waitFor(() => {
expect(screen.getByRole('region', { name: 'Sugestões' })).toBeTruthy()
expect(screen.getByText('Cairu')).toBeTruthy()
expect(screen.getByLabelText('Município prioritário')).toBeTruthy()
})
})
it('typing keeps search results with priority hits mounted (B102)', async () => {
vi.mocked(postCampaignJson).mockImplementation(async (_url, body) => {
if (typeof body === 'object' && body !== null && 'mode' in body && body.mode === 'search') {
return { ok: true, payload: searchWithPriorityPayload }
}
return { ok: true, payload: idleSuggestPayload }
})
renderQuickActionsChrome(<CampaignQuickActionsDrawer actions={[]} />)
const input = screen.getByLabelText('Buscar na campanha')
fireEvent.focus(input)
fireEvent.change(input, { target: { value: 'ca' } })
await waitFor(() => {
expect(postCampaignJson).toHaveBeenCalledWith(
'/campanha/home-search',
{ mode: 'search', query: 'ca' },
expect.any(AbortSignal),
)
})
await waitFor(() => {
expect(screen.getByRole('region', { name: 'Municípios' })).toBeTruthy()
expect(screen.getByText('Cairu')).toBeTruthy()
expect(screen.getByLabelText('Município prioritário')).toBeTruthy()
})
})
it('toggles between dock and collapsed from the handle', () => {
vi.mocked(postCampaignJson).mockResolvedValue({
ok: true,

View file

@ -667,3 +667,22 @@ describe('public site metadata global access', () => {
expect(offenders, 'import and call resolveSiteMetadata from @/utilities/seo').toEqual([])
})
})
describe('campaign TooltipProvider wraps quick-actions chrome (B102)', () => {
// B91/B100 mount the drawer as a sibling of the scrollport inside
// CampaignAppScrollChrome. Nesting TooltipProvider only around {children}
// left focus→suggest priority hits (CampaignHoverTooltip) without a
// provider and crashed the page.
it('lifts TooltipProvider around CampaignAppScrollChrome in the app layout', () => {
const layoutPath = resolve(repoRoot, 'src/app/(campaign)/campanha/(app)/layout.tsx')
expect(existsSync(layoutPath)).toBe(true)
const source = readFileSync(layoutPath, 'utf8')
expect(source).toMatch(
/<TooltipProvider[^>]*>\s*<CampaignAppScrollChrome[\s\S]*?<\/CampaignAppScrollChrome>\s*<\/TooltipProvider>/,
)
expect(source).not.toMatch(
/<CampaignAppScrollChrome[^>]*>\s*<TooltipProvider[\s\S]*?<\/TooltipProvider>\s*<\/CampaignAppScrollChrome>/,
)
})
})