From 9eafe6081c6afcc146f6239f6265398b48536c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 30 Jul 2026 12:39:35 -0300 Subject: [PATCH 1/4] feat(campanha): add Demandas group to home global search (B53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Staff can find demands by title from the Início omnibox with municipality and status in the secondary line, scoped by campaignDemand access. --- .cursor/rules/projects/nucleos-eleitorais.mdc | 1 + .../plans/busca-global-resultados-demandas.md | 4 +- .../campanha/(app)/home-search/route.ts | 5 +- src/app/(campaign)/campanha/(app)/page.tsx | 2 + .../dashboard/HomeSearchDemandGroup.tsx | 32 ++++++ src/lib/campaignHomeSearchHits.ts | 15 ++- .../homeSearch/loadHomeSearchSuggestions.ts | 1 + src/utilities/homeSearch/searchHomeDemands.ts | 100 +++++++++++++++++ .../homeSearch/searchHomeMunicipalities.ts | 2 + tests/int/homeSearchDemands.int.spec.ts | 105 ++++++++++++++++++ tests/unit/homeSearchUi.unit.spec.tsx | 1 + 11 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/components/campaign/dashboard/HomeSearchDemandGroup.tsx create mode 100644 src/utilities/homeSearch/searchHomeDemands.ts create mode 100644 tests/int/homeSearchDemands.int.spec.ts diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index f33ecadb..524f923e 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,6 +7,7 @@ alwaysApply: false ## Status +- **B53 entregue em código (2026-07-30):** sexto grupo da busca global do Início — **Demandas** via `searchHomeDemands` + `HomeSearchDemandGroup` (após Dobradinhas); `POST /campanha/home-search` agrega `demands`; match word-start no `title`; secundário `"{município} · {status}"`; staff access com `overrideAccess: false`; cap 25; int `homeSearchDemands.int.spec.ts`. WA = **B55**. Plano: [`busca-global-resultados-demandas.md`](../../../docs/plans/busca-global-resultados-demandas.md). Próximo na cadeia busca: **B54** grid/cap. - **B70 entregue em código (2026-07-30):** wizard atualizar liderança (A4 standalone) — `/campanha/acoes/atualizar-lideranca?municipio=` com grid 2/3/4 (`WizardLeadershipStep`), form curto de 6 campos (`WizardLeadershipForm`), skip via `trailingAction` quando `entryAction !== 'update-leadership'`, Continuar após Salvar dirty → Início + toast; `loadWizardLeadershipTiles`; writes `updateLeadershipWizard`/`createLeadershipWizard` (Contact+Leadership em transação); `WIZARD_ENTRY_ACTION_QUERY_KEY` em `campaignActionRoutes.ts`; `truncateNameAtWordBoundary`. Sem migration. Plano: [`wizard-atualizar-lideranca.md`](../../../docs/plans/wizard-atualizar-lideranca.md). - **B63 entregue em código (2026-07-30):** wizard registrar sinal (UX-1) — duas etapas no chassis B59: `WizardSignalTypeStep` (grid 2×3 de tiles transparentes + borda, ícone Lucide, Drawer bottom com `infoContent` provisório, `contentAlign="end"` no mobile) e `WizardSignalBodyStep` (textarea + Salvar explícito via `createMunicipalityListSignalFormAction` → `createMunicipalityUpdate` `kind: 'sinal'`). Contrato URL: `?municipio=` (B60) → `?signalType=` (tipo) → body; `?entry=` opcional — skip "Pular registro de sinal" visível só quando embutido (`entry` presente e ≠ `register-signal`), href skip → `/campanha` na v1. Helpers: `wizardSignalHref`, `resolveWizardSignalTypeParam`, `parseWizardEntryActionParam` em `campaignActionRoutes.ts`; `municipalitySignalTypeMeta.ts`; `wizardSignalUi.ts`. B63+ alinha `flowTitle`/`skip` ao chrome B75. Sem migration/Consent. Unit `campaignActionRoutes` + `wizardSignalUi`; e2e standalone + info não avança + skip embutido. Plano: [`wizard-registro-sinal.md`](../../../docs/plans/wizard-registro-sinal.md). Desbloqueia prefill de **B64** (soft). - **B75 entregue em código (2026-07-30):** header mobile do wizard — em `/campanha/acoes/*` no mobile (`< md`), o Mandate Red deixa de ser sidebar+marca e vira chrome de fluxo: título do catálogo B45 (`wizardFlowTitleForSlug`), subtítulo do município após B60, 1ª etapa só X (`aria-label="Sair da ação"`), 2ª+ Voltar+X; `skip` substitui X quando encadeado (B63+). `CampaignWizardChromeProvider` + `CampaignMobileTopBar` no layout `(app)`; `CampaignWizardShell` publica chrome via `useLayoutEffect` (handoff entre passos sem flash app-mode); desktop mantém caption muted no `
` (`md:block`). Helpers: `campaignWizardActionIdForSlug`, `wizardFlowTitleForActionId`, `toCampaignWizardChromeState`. `data-wizard-route` em `acoes/layout.tsx`. Unit `campaignMobileTopBar`, `campaignWizardShell`, `campaignWizardCopy`; e2e `campaignWizardChrome.e2e.spec.ts` (viewport 390). Débito adiado: skip no desktop; split read/write context do provider (gatilho: re-render medido no inset). Plano: [`header-mobile-wizard-campanha.md`](../../../docs/plans/header-mobile-wizard-campanha.md). Desbloqueia polish mobile de B63/B64/B70/B77/B78 sobre chrome único. diff --git a/docs/plans/busca-global-resultados-demandas.md b/docs/plans/busca-global-resultados-demandas.md index b8d971a3..306e60ad 100644 --- a/docs/plans/busca-global-resultados-demandas.md +++ b/docs/plans/busca-global-resultados-demandas.md @@ -1,7 +1,7 @@ # Busca global — resultados Demandas -Status: rascunho -Atualizado em: 2026-07-29 +Status: entregue (2026-07-30) +Atualizado em: 2026-07-30 — as-built: `searchHomeDemands` (`isStaffCampaignRole` gate; word-start no `title`; access `campaignDemand` com `overrideAccess: false`; cap 25; desempate `createdAt` desc); `HomeSearchDemandHit` + campo `demands` no `POST /campanha/home-search`; `HomeSearchDemandGroup` após Dobradinhas; secundário `"{município} · {status}"`; empty via `homeSearchHasAnyHits`; int `homeSearchDemands.int.spec.ts`; sem migration. Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B53 — busca global) Impeccable: B — grupo de hits no slot B47 Appetite: ~0,5 dia eng; loader + grupo; share = B55 diff --git a/src/app/(campaign)/campanha/(app)/home-search/route.ts b/src/app/(campaign)/campanha/(app)/home-search/route.ts index 86a3e537..1e46b29b 100644 --- a/src/app/(campaign)/campanha/(app)/home-search/route.ts +++ b/src/app/(campaign)/campanha/(app)/home-search/route.ts @@ -14,6 +14,7 @@ import { campaignJsonMutationRoute } from '@/utilities/campaignJsonMutationRoute import { loadHomeSearchSuggestions } from '@/utilities/homeSearch/loadHomeSearchSuggestions' import { searchHomeActivities } from '@/utilities/homeSearch/searchHomeActivities' import { searchHomeAdvisors } from '@/utilities/homeSearch/searchHomeAdvisors' +import { searchHomeDemands } from '@/utilities/homeSearch/searchHomeDemands' import { searchHomeLeaderships } from '@/utilities/homeSearch/searchHomeLeaderships' import { searchHomeMunicipalities } from '@/utilities/homeSearch/searchHomeMunicipalities' import { searchHomeStateDeputies } from '@/utilities/homeSearch/searchHomeStateDeputies' @@ -43,13 +44,14 @@ export const POST = campaignJsonMutationRoute( }) } - const [municipalityResult, advisors, leaderships, activities, stateDeputies] = + const [municipalityResult, advisors, leaderships, activities, stateDeputies, demands] = await Promise.all([ searchHomeMunicipalities(payload, actor, body.query), searchHomeAdvisors(payload, actor, body.query), searchHomeLeaderships(payload, actor, body.query), searchHomeActivities(payload, actor, body.query), searchHomeStateDeputies(payload, actor, body.query), + searchHomeDemands(payload, actor, body.query), ]) return NextResponse.json({ ...municipalityResult, @@ -57,6 +59,7 @@ export const POST = campaignJsonMutationRoute( leaderships, activities, stateDeputies, + demands, }) }, ) diff --git a/src/app/(campaign)/campanha/(app)/page.tsx b/src/app/(campaign)/campanha/(app)/page.tsx index 416edecb..40f8e953 100644 --- a/src/app/(campaign)/campanha/(app)/page.tsx +++ b/src/app/(campaign)/campanha/(app)/page.tsx @@ -8,6 +8,7 @@ import { CampaignHomeStaffChrome } from '@/components/campaign/dashboard/Campaig import { CampaignHomeSummary } from '@/components/campaign/dashboard/CampaignHomeSummary' import { HomeSearchActivityGroup } from '@/components/campaign/dashboard/HomeSearchActivityGroup' import { HomeSearchAdvisorGroup } from '@/components/campaign/dashboard/HomeSearchAdvisorGroup' +import { HomeSearchDemandGroup } from '@/components/campaign/dashboard/HomeSearchDemandGroup' import { HomeSearchLeadershipGroup } from '@/components/campaign/dashboard/HomeSearchLeadershipGroup' import { HomeSearchMunicipalityGroup } from '@/components/campaign/dashboard/HomeSearchMunicipalityGroup' import { HomeSearchResultsShell } from '@/components/campaign/dashboard/HomeSearchResultsShell' @@ -54,6 +55,7 @@ export default async function CampaignHomePage() { + } summarySlot={summarySlot} diff --git a/src/components/campaign/dashboard/HomeSearchDemandGroup.tsx b/src/components/campaign/dashboard/HomeSearchDemandGroup.tsx new file mode 100644 index 00000000..cf47fa85 --- /dev/null +++ b/src/components/campaign/dashboard/HomeSearchDemandGroup.tsx @@ -0,0 +1,32 @@ +'use client' + +import { HomeSearchHitRow } from '@/components/campaign/dashboard/HomeSearchHitRow' +import { useHomeSearchResults } from '@/components/campaign/dashboard/HomeSearchResultsContext' +import { homeSearchDemandGroupHasHits } from '@/lib/campaignHomeSearchHits' +import { HOME_SEARCH_GROUP_HEADING_CLASS, HOME_SEARCH_GROUP_LIST_CLASS } from '@/lib/homeSearchUi' + +export const HomeSearchDemandGroup = () => { + const { results } = useHomeSearchResults() + + if (results.status !== 'success') return null + if (!homeSearchDemandGroupHasHits(results.data)) return null + + const { demands } = results.data + + return ( +
+

Demandas

+
    + {demands.map((hit) => ( +
  • + +
  • + ))} +
+
+ ) +} diff --git a/src/lib/campaignHomeSearchHits.ts b/src/lib/campaignHomeSearchHits.ts index e979ca91..ae784999 100644 --- a/src/lib/campaignHomeSearchHits.ts +++ b/src/lib/campaignHomeSearchHits.ts @@ -46,6 +46,13 @@ export type HomeSearchActivityHit = { secondary: string } +export type HomeSearchDemandHit = { + id: number + slug: string + title: string + secondary: string +} + type HomeSearchResultKind = 'search' | 'suggest' export type HomeSearchSuccessResponse = { @@ -57,6 +64,7 @@ export type HomeSearchSuccessResponse = { leaderships: HomeSearchLeadershipHit[] stateDeputies: HomeSearchStateDeputyHit[] activities: HomeSearchActivityHit[] + demands: HomeSearchDemandHit[] } export const homeSearchMunicipalityGroupHasHits = ( @@ -75,6 +83,10 @@ export const homeSearchActivityGroupHasHits = ( data: Pick, ): boolean => data.activities.length > 0 +export const homeSearchDemandGroupHasHits = ( + data: Pick, +): boolean => data.demands.length > 0 + export const formatHomeSearchMunicipalityCount = (count: number): string => { if (count === 1) return '1 município' return `${count} municípios` @@ -99,7 +111,8 @@ export const homeSearchHasAnyHits = (data: HomeSearchSuccessResponse): boolean = data.advisors.length > 0 || homeSearchLeadershipGroupHasHits(data) || homeSearchStateDeputyGroupHasHits(data) || - homeSearchActivityGroupHasHits(data) + homeSearchActivityGroupHasHits(data) || + homeSearchDemandGroupHasHits(data) export type WizardMunicipalitySearchSuccessResponse = { status: 'success' diff --git a/src/utilities/homeSearch/loadHomeSearchSuggestions.ts b/src/utilities/homeSearch/loadHomeSearchSuggestions.ts index 5d2abb53..78eab864 100644 --- a/src/utilities/homeSearch/loadHomeSearchSuggestions.ts +++ b/src/utilities/homeSearch/loadHomeSearchSuggestions.ts @@ -83,5 +83,6 @@ export const loadHomeSearchSuggestions = async ( leaderships: [], stateDeputies: [], activities: [], + demands: [], } } diff --git a/src/utilities/homeSearch/searchHomeDemands.ts b/src/utilities/homeSearch/searchHomeDemands.ts new file mode 100644 index 00000000..19572af5 --- /dev/null +++ b/src/utilities/homeSearch/searchHomeDemands.ts @@ -0,0 +1,100 @@ +import 'server-only' + +import type { Payload } from 'payload' + +import { homeSearchQueryIsActive, normalizeHomeSearchRaw } from '@/lib/campaignHomeSearchContract' +import type { HomeSearchDemandHit } from '@/lib/campaignHomeSearchHits' +import { HOME_SEARCH_STAFF_ONLY_MESSAGE } from '@/lib/campaignHomeSearchMessages' +import { isStaffCampaignRole } from '@/lib/campaignRoles' +import { + compareHomeSearchNameRelevance, + normalizeHomeSearchName, +} from '@/lib/homeSearchMunicipalityMatch' +import { isPopulatedRelationship } from '@/lib/relationship' +import { campaignDemandStatusLabels, type CampaignDemandStatus } from '@/lib/schemas/campaignDemand' +import { matchesNormalizedAtWordStart } from '@/lib/wordStartFilter' +import type { CampaignDemand, CampaignUser, Municipality } from '@/payload-types' + +const HOME_SEARCH_DEMAND_HIT_CAP = 25 + +export const formatDemandHomeSearchSecondary = ( + municipalityName: string | null, + status: CampaignDemandStatus, +): string => { + const statusLabel = campaignDemandStatusLabels[status] + if (!municipalityName) return statusLabel + return `${municipalityName} · ${statusLabel}` +} + +export const searchHomeDemands = async ( + payload: Payload, + user: CampaignUser, + rawQuery: string, +): Promise => { + if (!isStaffCampaignRole(user.role)) { + throw new Error(HOME_SEARCH_STAFF_ONLY_MESSAGE) + } + + const query = normalizeHomeSearchRaw(rawQuery) + if (!homeSearchQueryIsActive(query)) { + return [] + } + + const normalizedQuery = normalizeHomeSearchName(query) + + const result = await payload.find({ + collection: 'campaignDemand', + where: { title: { contains: query } }, + depth: 1, + limit: 0, + pagination: false, + select: { + id: true, + title: true, + slug: true, + status: true, + municipality: true, + createdAt: true, + }, + user, + overrideAccess: false, + }) + + const candidates: Array<{ + normalizedName: string + tieBreakDesc: number + hit: HomeSearchDemandHit + }> = [] + + for (const doc of result.docs as CampaignDemand[]) { + const normalizedName = normalizeHomeSearchName(doc.title) + if (!matchesNormalizedAtWordStart(normalizedName, normalizedQuery)) { + continue + } + + const municipalityName = isPopulatedRelationship(doc.municipality) + ? doc.municipality.name + : null + + const tieBreakDesc = doc.createdAt ? Date.parse(doc.createdAt) : 0 + + candidates.push({ + normalizedName, + tieBreakDesc: Number.isFinite(tieBreakDesc) ? tieBreakDesc : 0, + hit: { + id: doc.id, + slug: doc.slug, + title: doc.title, + secondary: formatDemandHomeSearchSecondary( + municipalityName, + doc.status as CampaignDemandStatus, + ), + }, + }) + } + + return candidates + .sort((left, right) => compareHomeSearchNameRelevance(left, right, normalizedQuery)) + .slice(0, HOME_SEARCH_DEMAND_HIT_CAP) + .map((row) => row.hit) +} diff --git a/src/utilities/homeSearch/searchHomeMunicipalities.ts b/src/utilities/homeSearch/searchHomeMunicipalities.ts index 426da8f6..91efe139 100644 --- a/src/utilities/homeSearch/searchHomeMunicipalities.ts +++ b/src/utilities/homeSearch/searchHomeMunicipalities.ts @@ -29,6 +29,7 @@ const emptyResponse = (): HomeSearchSuccessResponse => ({ leaderships: [], stateDeputies: [], activities: [], + demands: [], }) export const searchHomeMunicipalities = async ( @@ -91,5 +92,6 @@ export const searchHomeMunicipalities = async ( leaderships: [], stateDeputies: [], activities: [], + demands: [], } } diff --git a/tests/int/homeSearchDemands.int.spec.ts b/tests/int/homeSearchDemands.int.spec.ts new file mode 100644 index 00000000..95877046 --- /dev/null +++ b/tests/int/homeSearchDemands.int.spec.ts @@ -0,0 +1,105 @@ +// @vitest-environment node + +import type { Payload } from 'payload' +import { getPayload } from 'payload' +import { beforeAll, describe, expect, it } from 'vitest' + +import config from '@/payload.config' +import { searchHomeDemands } from '@/utilities/homeSearch/searchHomeDemands' + +import { installCampaignFixtures } from '../helpers/campaignFixtures' + +let payload: Payload +const campaignFixtures = installCampaignFixtures({ + getPayload: () => payload, + setPayload: (nextPayload) => { + payload = nextPayload + }, +}) + +describe('searchHomeDemands (B53)', () => { + beforeAll(async () => { + payload = await getPayload({ config: await config }) + }) + + it('returns empty hits when query is shorter than the minimum', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + + const result = await searchHomeDemands(payload, coordinator, 'a') + expect(result).toEqual([]) + }) + + it('matches demands by word-start on the title for staff', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const municipality = await fixtures.getMunicipality('cairu') + const title = fixtures.value('Material Zeca Centro') + const demand = await fixtures.createCampaignDemand({ + municipality, + title, + createdBy: coordinator, + }) + + const result = await searchHomeDemands(payload, coordinator, 'Material') + const hit = result.find((row) => row.id === demand.id) + expect(hit?.title).toBe(title) + expect(hit?.slug).toBe(demand.slug) + expect(hit?.secondary).toContain('Cairu') + expect(hit?.secondary).toContain('Aberta') + }) + + it('does not match mid-word without word-start boundary', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const municipality = await fixtures.getMunicipality('cairu') + const title = fixtures.value('Transporte Comunitário') + const demand = await fixtures.createCampaignDemand({ + municipality, + title, + createdBy: coordinator, + }) + + const result = await searchHomeDemands(payload, coordinator, 'ransporte') + expect(result.some((row) => row.id === demand.id)).toBe(false) + }) + + it('scopes demand hits to the advisor portfolio by municipality', async () => { + const fixtures = campaignFixtures() + const advisor = await fixtures.createCampaignUser('advisor') + const coordinator = await fixtures.createCampaignUser('coordinator') + + const administered = await fixtures.getMunicipality('cairu') + const other = await fixtures.getMunicipality('feira-de-santana') + await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id]) + + const scopeToken = fixtures.value('scope-demanda') + const inScope = await fixtures.createCampaignDemand({ + municipality: administered, + title: `Material ${scopeToken} Cairu`, + createdBy: coordinator, + }) + const outOfScope = await fixtures.createCampaignDemand({ + municipality: other, + title: `Material ${scopeToken} Feira`, + createdBy: coordinator, + }) + + const advisorResult = await searchHomeDemands(payload, advisor, scopeToken) + expect(advisorResult.map((hit) => hit.id)).toEqual([inScope.id]) + + const coordinatorResult = await searchHomeDemands(payload, coordinator, scopeToken) + expect(coordinatorResult.map((hit) => hit.id).sort((a, b) => a - b)).toEqual( + [inScope.id, outOfScope.id].sort((a, b) => a - b), + ) + }) + + it('rejects leaders from home search', async () => { + const fixtures = campaignFixtures() + const leader = await fixtures.createCampaignUser('leader') + + await expect(searchHomeDemands(payload, leader, 'Material')).rejects.toThrow( + /equipe de campanha/i, + ) + }) +}) diff --git a/tests/unit/homeSearchUi.unit.spec.tsx b/tests/unit/homeSearchUi.unit.spec.tsx index 6782727e..59a73654 100644 --- a/tests/unit/homeSearchUi.unit.spec.tsx +++ b/tests/unit/homeSearchUi.unit.spec.tsx @@ -26,6 +26,7 @@ const searchResultsValue = ( leaderships: [], stateDeputies: [], activities: [], + demands: [], ...data, }, }, From 914fb5a7e7af2948e994844cb6aa9c031a6f96c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 30 Jul 2026 12:43:47 -0300 Subject: [PATCH 2/4] fix(campanha): keep demand search secondary formatter module-private Knip flags exported helpers with no external call sites. --- src/utilities/homeSearch/searchHomeDemands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/homeSearch/searchHomeDemands.ts b/src/utilities/homeSearch/searchHomeDemands.ts index 19572af5..e5fc8c6c 100644 --- a/src/utilities/homeSearch/searchHomeDemands.ts +++ b/src/utilities/homeSearch/searchHomeDemands.ts @@ -17,7 +17,7 @@ import type { CampaignDemand, CampaignUser, Municipality } from '@/payload-types const HOME_SEARCH_DEMAND_HIT_CAP = 25 -export const formatDemandHomeSearchSecondary = ( +const formatDemandHomeSearchSecondary = ( municipalityName: string | null, status: CampaignDemandStatus, ): string => { From 5b4ab9c0200050fca7b15b2b087293b909f67dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 30 Jul 2026 13:25:49 -0300 Subject: [PATCH 3/4] =?UTF-8?q?fix(ci):=20unblock=20PR=20checks=20?= =?UTF-8?q?=E2=80=94=20checkout=20in=20migration-lock,=20seed-proof=20lead?= =?UTF-8?q?ership=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit migration-lock ran `gh pr list --json files` without actions/checkout, so every PR failed the job in 3s with "not a git repository". The leadership portfolio spec asserted the exact global hit set with fixed names, which the minimal seed (Seed Liderança Um/Dois) collides with via word-start 'Lider'; switch to the unique-token pattern already used by the demands spec. --- .github/workflows/ci-pr.yml | 3 +++ tests/int/homeSearchLeaderships.int.spec.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 98c730f0..2dcc477a 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -88,6 +88,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + # gh expands --json files via git, so it needs the repo checked out. + - uses: actions/checkout@v4 + - name: Count open PRs touching schema files env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/int/homeSearchLeaderships.int.spec.ts b/tests/int/homeSearchLeaderships.int.spec.ts index 49fbe6be..0a80c878 100644 --- a/tests/int/homeSearchLeaderships.int.spec.ts +++ b/tests/int/homeSearchLeaderships.int.spec.ts @@ -69,8 +69,11 @@ describe('searchHomeLeaderships (B49)', () => { const other = await fixtures.getMunicipality('feira-de-santana') await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id]) - const inScope = await fixtures.createContact({ name: 'Lider Cairu' }) - const outOfScope = await fixtures.createContact({ name: 'Lider Feira' }) + // Unique token per run: the minimal seed ships leaderships whose contact + // names collide with fixed queries like 'Lider' (word-start match). + const scopeToken = fixtures.value('scope-lideranca') + const inScope = await fixtures.createContact({ name: `${scopeToken} Cairu` }) + const outOfScope = await fixtures.createContact({ name: `${scopeToken} Feira` }) const scopedLeadership = await fixtures.createLeadership({ contact: inScope.id, municipalities: [administered.id], @@ -80,10 +83,10 @@ describe('searchHomeLeaderships (B49)', () => { municipalities: [other.id], }) - const advisorResult = await searchHomeLeaderships(payload, advisor, 'Lider') + const advisorResult = await searchHomeLeaderships(payload, advisor, scopeToken) expect(advisorResult.map((hit) => hit.id)).toEqual([scopedLeadership.id]) - const coordinatorResult = await searchHomeLeaderships(payload, coordinator, 'Lider') + const coordinatorResult = await searchHomeLeaderships(payload, coordinator, scopeToken) expect(coordinatorResult.map((hit) => hit.id).sort((a, b) => a - b)).toEqual( [scopedLeadership.id, otherLeadership.id].sort((a, b) => a - b), ) From 76a03fac91a18bbac99bef7652dba7292e7d2f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 30 Jul 2026 13:37:31 -0300 Subject: [PATCH 4/4] fix(ci): grant migration-lock pull-requests read on GITHUB_TOKEN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checkout fixed the missing .git, but `gh pr list` then failed with "GraphQL: Resource not accessible by integration (repository.pullRequests)" — the default token needs an explicit permission grant. --- .github/workflows/ci-pr.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 2dcc477a..239b750c 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -87,6 +87,10 @@ jobs: # chain; the second PR must wait or rebase. runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + # gh pr list against the default GITHUB_TOKEN. + contents: read + pull-requests: read steps: # gh expands --json files via git, so it needs the repo checked out. - uses: actions/checkout@v4