teqo/tests/int/municipalityTicketPartnerData.int.spec.ts
Francisco Solla 8dd60cf962 A6 — Dobradinha 2026 automática quando o TSE publicar candidaturas (pós-15/08) (#82)
* docs(A6): freshness audit da #27 no plano de dobradinhas 2026

electionInsights.ts foi deletado no Pass 2 W4a (modulo puro novo),
electionAlliances.ts nao nasce (campoParties + electionPartySpectrum ja
carregam a taxonomia), questoes em aberto resolvidas (tiers, score,
proporcionais, lockdown de lideranca), card na aba Eleicoes, overview da
lista fora de escopo, cache election-tse com bust na conciliacao Fase 5.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

* feat(A6): insight de dobradinhas 2026 na aba Eleicoes do municipio

Lista ranqueada de candidatos proporcionais que concorrem de novo em 2026
(runningAgain2026=sim) por geografia do municipio: score 0,6 alinhamento
(campo 2026 + espectro Bolognesi) + 0,4 forca local normalizada pelo maximo
do conjunto. Estado pendente ate a conciliacao pos-15/08; Solla excluido
(referencia, nao parceiro); leitura derivada sem escrita; loader cacheado
sob a tag election-tse como os irmaos; link para /campanha/dobradinhas
como caminho de registro operacional.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

* test(A6): cobertura unit/int do insight + lease das colecoes eleitorais

Unit: tiers (campo/espectro/falha-para-o-meio), score, ordenacao, empates,
limite, zero votos. Int: pending2026 sem conciliacao (mesmo com votos
locais), so adversarios, misto com exclusao de Solla/nao e geografia
alheia. Lease advisory election-collections serializa os dois specs que
tratam as colecoes eleitorais como test-owned (o import wipe-all apagaria
as fixtures do novo spec no meio do assert, 48-way paralelo).

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

* refactor(A6): fixes do /simplify (3 reviewers paralelos)

P1: campo coalition morto removido do input/select/mapping (sem consumidor).
P1: hooks do lease com timeout explicito de 120s nos dois specs eleitorais
(o wait advisory passa dos 10s default do Vitest sob contencao — flake em
ambas as direcoes). P2: sonda de conciliacao via payload.count (idioma da
casa), desempate final por candidateNumber (ordem deterministica), geography
como prop na secao (sem lookup duplicado), casing Dep. Federal/Estadual
alinhado, marcador 1313 via BASELINE_TICKET_2022, entrada no CHANGELOG.

Declinados com justificativa: zones como tupla nao-vazia (churn em tipo
compartilhado por assert provada no catalogo) e early-start da promise
antes do Promise.all (loader-dentro-do-Suspense e o idioma da casa; ganho
so em cache frio).

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 00:14:27 -03:00

266 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment node
import { getPayload, type Payload } from 'payload'
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
import { BASELINE_TICKET_2022, ELECTION_YEAR_2022 } from '@/lib/electionResults'
import { municipalityCatalog } from '@/lib/municipalityCatalog'
import type { ElectionCandidate } from '@/payload-types'
import config from '@/payload.config'
import {
municipalityElectionGeography,
type MunicipalityElectionGeography,
} from '@/utilities/municipality/municipalityElectionGeography'
import { queryMunicipalityTicketPartners } from '@/utilities/municipality/municipalityTicketPartnerData'
import {
acquireTestDatabaseLease,
ELECTION_COLLECTIONS_LEASE_KEY,
type TestDatabaseLease,
} from '../helpers/testDatabaseLease'
/**
* A6 — dobradinha opportunities loader. Election collections are test-owned in
* the int suite (electionResultsImport wipes them), so this spec holds the
* shared lease across the whole file. Marker candidate numbers live at
* 900001+ — outside any real TSE number range (4–5 digits) — plus Solla's own
* 1313, which the exclusion test must use by definition; both marker sets are
* deleted before and after the file, so a full-seed dev database loses at
* most those rows (the import spec already wipes everything).
*/
const MARKER_CANDIDATE_NUMBERS = [
900001,
900002,
900003,
900004,
900005,
900006,
900007,
BASELINE_TICKET_2022.candidate.candidateNumber,
]
const geographyOf = (index: number): MunicipalityElectionGeography & { cityName: string } => {
const entry = municipalityCatalog[index]
if (!entry) throw new Error(`municipalityCatalog[${index}] is unavailable.`)
return { ...municipalityElectionGeography(entry), cityName: entry.name }
}
const geographyA = geographyOf(0)
const geographyB = geographyOf(1)
let payload: Payload
let lease: TestDatabaseLease
const deleteMarkerRows = async () => {
await payload.delete({
collection: 'electionCandidateVote',
where: { candidateNumber: { in: MARKER_CANDIDATE_NUMBERS } },
depth: 0,
overrideAccess: true,
})
await payload.delete({
collection: 'electionCandidate',
where: { candidateNumber: { in: MARKER_CANDIDATE_NUMBERS } },
depth: 0,
overrideAccess: true,
})
// Any reconciled row (sim/nao) flips the pending2026 probe statewide. The
// int suite treats election data as disposable, so reset the flag to keep
// the first scenario deterministic on every machine.
await payload.update({
collection: 'electionCandidate',
where: { runningAgain2026: { in: ['sim', 'nao'] } },
data: { runningAgain2026: 'desconhecido' },
overrideAccess: true,
})
}
type CandidateInput = {
office: ElectionCandidate['office']
candidateNumber: number
urnaName: string
party: string
runningAgain2026: ElectionCandidate['runningAgain2026']
elected?: boolean
}
const createCandidate = (input: CandidateInput) =>
payload.create({
collection: 'electionCandidate',
data: {
year: ELECTION_YEAR_2022,
turn: '1',
state: 'BA',
coalition: null,
elected: input.elected ?? false,
...input,
},
depth: 0,
overrideAccess: true,
})
const createVotes = (
geography: MunicipalityElectionGeography & { cityName: string },
office: ElectionCandidate['office'],
candidateNumber: number,
candidateName: string,
votes: number,
) =>
payload.create({
collection: 'electionCandidateVote',
data: {
year: ELECTION_YEAR_2022,
office,
turn: '1',
state: 'BA',
cityCode: geography.cityCode,
cityName: geography.cityName,
zoneNumber: geography.zones[0]!,
candidateNumber,
candidateName,
voteType: 'nominal',
votes,
},
depth: 0,
overrideAccess: true,
})
describe('municipality ticket partner data (A6)', () => {
// The lease acquisition blocks until the other election spec finishes its
// whole file — far past the 10s default hook timeout under pool contention.
beforeAll(async () => {
payload = await getPayload({ config: await config })
lease = await acquireTestDatabaseLease(payload, ELECTION_COLLECTIONS_LEASE_KEY)
await deleteMarkerRows()
}, 120_000)
afterEach(async () => {
await deleteMarkerRows()
})
afterAll(async () => {
await deleteMarkerRows()
await lease.release()
}, 120_000)
it('stays unavailable while 2026 is not reconciled, even with local 2022 votes', async () => {
await createCandidate({
office: 'deputado_federal',
candidateNumber: 900001,
urnaName: 'Candidato Desconhecido',
party: 'PT',
runningAgain2026: 'desconhecido',
})
await createVotes(geographyA, 'deputado_federal', 900001, 'Candidato Desconhecido', 500)
const result = await queryMunicipalityTicketPartners(payload, geographyA)
expect(result).toEqual({ status: 'pending2026' })
})
it('lists only adversaries when every returning candidate is one', async () => {
await createCandidate({
office: 'deputado_federal',
candidateNumber: 900002,
urnaName: 'Adversário Federal',
party: 'PL',
runningAgain2026: 'sim',
})
await createCandidate({
office: 'deputado_estadual',
candidateNumber: 900003,
urnaName: 'Adversário Estadual',
party: 'MDB',
runningAgain2026: 'sim',
})
await createVotes(geographyA, 'deputado_federal', 900002, 'Adversário Federal', 800)
await createVotes(geographyA, 'deputado_estadual', 900003, 'Adversário Estadual', 500)
const result = await queryMunicipalityTicketPartners(payload, geographyA)
expect(result.status).toBe('ready')
if (result.status !== 'ready') return
expect(result.opportunities.map((opportunity) => opportunity.name)).toEqual([
'Adversário Federal',
'Adversário Estadual',
])
expect(result.opportunities.every((opportunity) => opportunity.tier === 'adversario')).toBe(
true,
)
expect(result.opportunities[0]?.votes2022).toBe(800)
expect(result.opportunities[1]?.votes2022).toBe(500)
})
it('ranks a mixed field, excludes Solla/nao, and ignores votes from other geographies', async () => {
await createCandidate({
office: 'deputado_estadual',
candidateNumber: 900004,
urnaName: 'Aliada Estadual',
party: 'PT',
runningAgain2026: 'sim',
elected: true,
})
await createCandidate({
office: 'deputado_federal',
candidateNumber: 900005,
urnaName: 'Histórico Federal',
party: 'PSOL',
runningAgain2026: 'sim',
})
await createCandidate({
office: 'deputado_federal',
candidateNumber: 900006,
urnaName: 'Adversário Federal',
party: 'PL',
runningAgain2026: 'sim',
})
// Solla himself: reference, never a partner row.
await createCandidate({
office: 'deputado_federal',
candidateNumber: BASELINE_TICKET_2022.candidate.candidateNumber,
urnaName: 'Jorge Solla',
party: 'PT',
runningAgain2026: 'sim',
})
// Reconciled as NOT running again: excluded.
await createCandidate({
office: 'deputado_federal',
candidateNumber: 900007,
urnaName: 'Aposentado Federal',
party: 'PV',
runningAgain2026: 'nao',
})
await createVotes(geographyA, 'deputado_estadual', 900004, 'Aliada Estadual', 100)
await createVotes(geographyB, 'deputado_estadual', 900004, 'Aliada Estadual', 500)
await createVotes(geographyA, 'deputado_federal', 900005, 'Histórico Federal', 9000)
await createVotes(geographyA, 'deputado_federal', 900006, 'Adversário Federal', 9000)
await createVotes(
geographyA,
'deputado_federal',
BASELINE_TICKET_2022.candidate.candidateNumber,
'Jorge Solla',
9999,
)
await createVotes(geographyA, 'deputado_federal', 900007, 'Aposentado Federal', 9999)
const result = await queryMunicipalityTicketPartners(payload, geographyA)
expect(result.status).toBe('ready')
if (result.status !== 'ready') return
expect(result.opportunities.map((opportunity) => opportunity.name)).toEqual([
'Histórico Federal',
'Aliada Estadual',
'Adversário Federal',
])
expect(result.opportunities.map((opportunity) => opportunity.tier)).toEqual([
'aliadoHistorico',
'aliado',
'adversario',
])
// Geography B votes do not leak into the municipality total.
const aliada = result.opportunities.find(
(opportunity) => opportunity.candidateNumber === 900004,
)
expect(aliada?.votes2022).toBe(100)
expect(aliada?.elected2022).toBe(true)
})
})