teqo-backup/tests/unit/sectionPotential.unit.spec.ts
Francisco Solla 62b674cfba feat(potencial): cálculo puro, loader e rotas públicas da seção
sectionPotential.ts parseia/valida o shard (ordem fail-closed), busca por
seção e computa o cenário aprovado — válidos = nominais, X₁ = comparecimento
− Lula − Flávio (todos os terceiros; o bulk tem 13 candidaturas) e X₂ =
aptos − Lula − Flávio — com formatadores pt-BR e o nome do PNG. O loader
server-only lê os shards por fs com cache por processo; as rotas públicas
/api/potencial/{municipios,secao} aplicam same-origin, rate-limit e zod
strict, com 404 honesto e cache de um dia. O story 1080×1920 é desenhado
por um renderer puro com contexto estrutural. 'potencial' entra nos slugs
reservados e o módulo novo no pin do top-level de utilities.
2026-10-07 19:47:19 -03:00

206 lines
7.1 KiB
TypeScript
Raw Permalink 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.

import { describe, expect, it } from 'vitest'
import {
computeSectionPotential,
filterPotentialMunicipalities,
findPotentialSection,
formatPotentialPercent,
formatPotentialPoints,
parsePotentialShard,
sectionStoryFileName,
type PotentialSectionNumbers,
type PotentialUfShard,
} from '@/lib/sectionPotential'
const numbers = (overrides: Partial<PotentialSectionNumbers> = {}): PotentialSectionNumbers => ({
uf: 'BA',
municipalityCode: 39098,
municipalityName: 'Serrinha',
zone: 150,
section: 50,
aptos: 376,
comparecimento: 289,
lula: 174,
flavio: 76,
validos: 260,
...overrides,
})
describe('computeSectionPotential', () => {
it('refuses the plan illustrative reference (válidos above the comparecimento)', () => {
// O exemplo do plano ("Seção 0051 Curitiba/PR: Lula 91, Flávio 81, válidos
// 190, X₁=13, X₂=69") é uma referência de forma, marcada como "não usar na
// UI como dado real" — e é aritmeticamente impossível: comp = Lula+Flávio+X₁
// = 185 < válidos = 190. O cálculo falha fechado em vez de renderizar %.
const result = computeSectionPotential({
aptos: 241,
comparecimento: 185,
lula: 91,
flavio: 81,
validos: 190,
})
expect(result).toEqual({ available: false, reason: 'no-data' })
})
it('computes the real Serrinha/BA ZE 150 section 50 row', () => {
const result = computeSectionPotential(numbers())
expect(result.available).toBe(true)
if (!result.available) return
// X₁ = 289 − 174 − 76 = 39 (nulos + brancos + todos os terceiros);
// X₂ = 376 − 174 − 76 = 126 (X₁ + faltantes).
expect(result.potential.x1).toBe(39)
expect(result.potential.x2).toBe(126)
expect(formatPotentialPercent(result.potential.firstRoundPct)).toBe('66,9%')
expect(formatPotentialPercent(result.potential.immediatePct)).toBe('73,7%')
expect(formatPotentialPercent(result.potential.totalPct)).toBe('79,8%')
expect(formatPotentialPoints(result.potential.gainImmediatePp)).toBe('+6,8 p.p.')
expect(formatPotentialPoints(result.potential.gainTotalPp)).toBe('+12,9 p.p.')
})
it('never fabricates a percentage for a section without valid votes', () => {
expect(
computeSectionPotential({ aptos: 100, comparecimento: 0, lula: 0, flavio: 0, validos: 0 }),
).toEqual({ available: false, reason: 'no-data' })
})
it.each([
[
'Lula + Flávio above the comparecimento',
{ aptos: 400, comparecimento: 100, lula: 60, flavio: 60, validos: 100 },
],
[
'válidos above the comparecimento',
{ aptos: 400, comparecimento: 100, lula: 40, flavio: 40, validos: 120 },
],
[
'turnout above the electorate',
{ aptos: 90, comparecimento: 100, lula: 40, flavio: 40, validos: 80 },
],
[
'a candidate above the valid votes',
{ aptos: 400, comparecimento: 300, lula: 250, flavio: 40, validos: 200 },
],
])('fails closed on impossible counts: %s', (_label, row) => {
expect(computeSectionPotential(row)).toEqual({ available: false, reason: 'no-data' })
})
it('proves the gain is never negative under the approved hypothesis', () => {
let seed = 7
const random = (max: number) => {
seed = (seed * 1103515245 + 12345) % 2147483648
return seed % (max + 1)
}
for (let index = 0; index < 500; index += 1) {
const aptos = 50 + random(950)
const comparecimento = random(aptos)
const validos = random(comparecimento)
const lula = random(validos)
const flavio = random(validos - lula)
const result = computeSectionPotential({ aptos, comparecimento, lula, flavio, validos })
if (!result.available) continue
const { gainImmediatePp, gainTotalPp, firstRoundPct, immediatePct, totalPct } =
result.potential
expect(gainImmediatePp).toBeGreaterThanOrEqual(0)
expect(gainTotalPp).toBeGreaterThanOrEqual(gainImmediatePp)
expect(firstRoundPct).toBeGreaterThanOrEqual(0)
expect(totalPct).toBeLessThanOrEqual(100)
expect(immediatePct).toBeLessThanOrEqual(100)
}
})
})
describe('parsePotentialShard', () => {
const valid = {
version: 1,
uf: 'BA',
municipalities: [
{ code: 39098, name: 'Serrinha', sections: [[150, 50, 376, 289, 174, 76, 260]] },
],
}
it('accepts a well-formed shard', () => {
expect(parsePotentialShard(valid)).toEqual(valid)
})
it.each([
['a wrong version', { ...valid, version: 2 }],
['an unknown UF', { ...valid, uf: 'XX' }],
['a municipality without a name', { ...valid, municipalities: [{ code: 1, sections: [] }] }],
[
'a row with the wrong width',
{ ...valid, municipalities: [{ code: 1, name: 'X', sections: [[1, 2, 3]] }] },
],
[
'a row with a negative count',
{ ...valid, municipalities: [{ code: 1, name: 'X', sections: [[1, 2, -3, 4, 5, 6, 7]] }] },
],
['a non-object', null],
])('rejects %s', (_label, value) => {
expect(parsePotentialShard(value)).toBeNull()
})
})
describe('findPotentialSection', () => {
const shard: PotentialUfShard = {
version: 1,
uf: 'BA',
municipalities: [
{ code: 39098, name: 'Serrinha', sections: [[150, 50, 376, 289, 174, 76, 260]] },
{ code: 40000, name: 'Outro', sections: [[1, 1, 10, 8, 4, 2, 7]] },
],
}
it('finds a section through the binary searches', () => {
expect(
findPotentialSection(shard, { municipalityCode: 39098, zone: 150, section: 50 }),
).toEqual({
uf: 'BA',
municipalityCode: 39098,
municipalityName: 'Serrinha',
zone: 150,
section: 50,
aptos: 376,
comparecimento: 289,
lula: 174,
flavio: 76,
validos: 260,
})
})
it('answers null for an unknown município or section', () => {
expect(findPotentialSection(shard, { municipalityCode: 1, zone: 1, section: 1 })).toBeNull()
expect(findPotentialSection(shard, { municipalityCode: 39098, zone: 1, section: 1 })).toBeNull()
})
})
describe('formatters and helpers', () => {
it('formats percentages with one pt-BR decimal', () => {
expect(formatPotentialPercent(66.923)).toBe('66,9%')
expect(formatPotentialPercent(0)).toBe('0,0%')
})
it('derives the sign of the gain instead of hardcoding "+"', () => {
expect(formatPotentialPoints(6.864)).toBe('+6,9 p.p.')
expect(formatPotentialPoints(0)).toBe('+0,0 p.p.')
expect(formatPotentialPoints(-1.24)).toBe('−1,2 p.p.')
})
it('names the story file from the section identity', () => {
expect(sectionStoryFileName(numbers())).toBe('potencial-lula-ba-serrinha-z150-s50.png')
})
it('filters municípios accent-insensitively from three letters', () => {
const municipalities = [
{ code: 1, name: 'Serrinha' },
{ code: 2, name: 'São Gonçalo dos Campos' },
{ code: 3, name: 'Feira de Santana' },
]
expect(filterPotentialMunicipalities(municipalities, 'ser')).toEqual([
{ code: 1, name: 'Serrinha' },
])
expect(filterPotentialMunicipalities(municipalities, 'sao goncalo')).toEqual([
{ code: 2, name: 'São Gonçalo dos Campos' },
])
expect(filterPotentialMunicipalities(municipalities, 'se')).toEqual([])
})
})