mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Fold duplicated campanha access, loaders, list/filter/form shells, and scripts into shared modules; reorganize utilities by domain; stabilize e2e; and document prevention-first audit workflow so the same smells cannot creep back in.
232 lines
9.6 KiB
TypeScript
232 lines
9.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
|
||
import { municipalityCatalog } from '@/lib/municipalityCatalog'
|
||
import { TERRITORIAL_CLASS_ANCHORS } from '@/lib/territorialClassAnchors'
|
||
import {
|
||
classifyMunicipalityTerritory,
|
||
computeAggregateTerritorialClass,
|
||
computeMunicipalityTerritorialClass,
|
||
TERRITORIAL_CLASSES,
|
||
territorialClassSortWeight,
|
||
type MunicipalityTerritorialClass,
|
||
type TerritorialClassInput,
|
||
} from '@/utilities/municipality/municipalityTerritorialClass'
|
||
|
||
/**
|
||
* Statewide reference: 100.000 own votes over 5.000.000 valid votes = a 2%
|
||
* statewide share, so a município with a 4% local share has LQ 2 (reduto) and
|
||
* one with 0,5% has LQ 0,25 (weak half).
|
||
*/
|
||
const input = (overrides: Partial<TerritorialClassInput> = {}): TerritorialClassInput => ({
|
||
ownVotes: 200,
|
||
validVotes: 10_000,
|
||
stateOwnVotes: 100_000,
|
||
stateValidVotes: 5_000_000,
|
||
fieldHeadroom: 1_000,
|
||
fieldHeadroomCut: 2_000,
|
||
captureRate: 0.1,
|
||
inCoreBlock: false,
|
||
...overrides,
|
||
})
|
||
|
||
describe('classifyMunicipalityTerritory', () => {
|
||
it('classifies as reduto when the local share is at least strongLq × his own standard', () => {
|
||
// 4% local vs 2% statewide = LQ 2
|
||
const result = classifyMunicipalityTerritory(input({ ownVotes: 400, validVotes: 10_000 }))
|
||
expect(result.class).toBe('reduto')
|
||
expect(result.lq).toBe(TERRITORIAL_CLASS_ANCHORS.strongLq)
|
||
expect(result.factors.slice(0, 2).map((factor) => factor.id)).toEqual(['dominance', 'ownShare'])
|
||
})
|
||
|
||
it('keeps a tiny reduto as reduto, with ownShare telling it apart from one that carries the account', () => {
|
||
const symbolic = classifyMunicipalityTerritory(input({ ownVotes: 60, validVotes: 1_000 }))
|
||
expect(symbolic.class).toBe('reduto')
|
||
// 60 / 100.000 = 0,06% of his statewide vote — the "reduto simbólico" of P10.
|
||
expect(symbolic.ownShare).toBeCloseTo(0.0006, 6)
|
||
})
|
||
|
||
it('classifies as manutenção inside the standard band', () => {
|
||
// 2% local = LQ 1
|
||
expect(classifyMunicipalityTerritory(input({ ownVotes: 200 })).class).toBe('manutencao')
|
||
// Just under the strong cut stays manutenção (boundary is inclusive on reduto)
|
||
expect(classifyMunicipalityTerritory(input({ ownVotes: 399 })).class).toBe('manutencao')
|
||
// Exactly at the weak cut is still manutenção (boundary is exclusive on marginal)
|
||
expect(classifyMunicipalityTerritory(input({ ownVotes: 100 })).lq).toBe(
|
||
TERRITORIAL_CLASS_ANCHORS.weakLq,
|
||
)
|
||
expect(classifyMunicipalityTerritory(input({ ownVotes: 100 })).class).toBe('manutencao')
|
||
})
|
||
|
||
it('classifies as expansão when he is weak but the field still has votes to win', () => {
|
||
const result = classifyMunicipalityTerritory(
|
||
input({ ownVotes: 50, fieldHeadroom: 9_000, fieldHeadroomCut: 2_000 }),
|
||
)
|
||
expect(result.class).toBe('expansao')
|
||
expect(result.factors[0]).toEqual({ id: 'field', value: 9_000 })
|
||
})
|
||
|
||
it('classifies as marginal when he is weak and the field is small', () => {
|
||
expect(
|
||
classifyMunicipalityTerritory(
|
||
input({ ownVotes: 50, fieldHeadroom: 100, fieldHeadroomCut: 2_000 }),
|
||
).class,
|
||
).toBe('marginal')
|
||
})
|
||
|
||
it('never calls a core-block município marginal, even with a small field', () => {
|
||
const result = classifyMunicipalityTerritory(
|
||
input({ ownVotes: 50, fieldHeadroom: 100, fieldHeadroomCut: 2_000, inCoreBlock: true }),
|
||
)
|
||
expect(result.class).toBe('expansao')
|
||
expect(result.inCoreBlock).toBe(true)
|
||
})
|
||
|
||
it('drops the capture factor when the field ceiling is unknown', () => {
|
||
const result = classifyMunicipalityTerritory(input({ captureRate: null }))
|
||
expect(result.factors.map((factor) => factor.id)).not.toContain('capture')
|
||
})
|
||
|
||
it('returns sem_base without a local electorate or a statewide denominator', () => {
|
||
expect(classifyMunicipalityTerritory(input({ validVotes: 0 })).class).toBe('sem_base')
|
||
expect(classifyMunicipalityTerritory(input({ stateOwnVotes: 0 })).class).toBe('sem_base')
|
||
expect(classifyMunicipalityTerritory(input({ stateValidVotes: 0 })).class).toBe('sem_base')
|
||
expect(classifyMunicipalityTerritory(input({ validVotes: 0 })).lq).toBeNull()
|
||
expect(classifyMunicipalityTerritory(input({ validVotes: 0 })).factors).toEqual([])
|
||
})
|
||
|
||
it('classifies a município with zero own votes as weak, not as an error', () => {
|
||
const result = classifyMunicipalityTerritory(input({ ownVotes: 0, fieldHeadroom: 9_000 }))
|
||
expect(result.class).toBe('expansao')
|
||
expect(result.lq).toBe(0)
|
||
})
|
||
})
|
||
|
||
describe('computeMunicipalityTerritorialClass over the committed artifact', () => {
|
||
const classifications = municipalityCatalog.map((entry) =>
|
||
computeMunicipalityTerritorialClass(entry.slug),
|
||
)
|
||
|
||
/**
|
||
* The regression this whole item exists to prevent: the inherited absolute
|
||
* thresholds (35/20/10) collapsed the entire state into one class, because a
|
||
* federal-deputy candidate's local share is 1–5% everywhere. Relative
|
||
* anchors must produce a usable spread over the real 435 municípios.
|
||
*/
|
||
it('produces a non-degenerate distribution over the 435 catalog slugs', () => {
|
||
const counts = new Map<MunicipalityTerritorialClass, number>()
|
||
for (const classification of classifications) {
|
||
counts.set(classification.class, (counts.get(classification.class) ?? 0) + 1)
|
||
}
|
||
|
||
for (const territorialClass of TERRITORIAL_CLASSES) {
|
||
if (territorialClass === 'sem_base') continue
|
||
expect(counts.get(territorialClass) ?? 0).toBeGreaterThan(0)
|
||
}
|
||
for (const [, count] of counts) {
|
||
expect(count / classifications.length).toBeLessThan(0.7)
|
||
}
|
||
})
|
||
|
||
it('gives every classified município a "por quê" and a finite LQ', () => {
|
||
for (const classification of classifications) {
|
||
if (classification.class === 'sem_base') continue
|
||
expect(classification.factors.length).toBeGreaterThanOrEqual(2)
|
||
expect(Number.isFinite(classification.lq ?? Number.NaN)).toBe(true)
|
||
}
|
||
})
|
||
|
||
it('keeps the core block at roughly half of his statewide vote', () => {
|
||
const coreShare = classifications
|
||
.filter((classification) => classification.inCoreBlock)
|
||
.reduce((total, classification) => total + classification.ownShare, 0)
|
||
expect(coreShare).toBeGreaterThan(0.4)
|
||
expect(coreShare).toBeLessThan(0.6)
|
||
})
|
||
|
||
it('memoizes: the same slug returns the identical object', () => {
|
||
const slug = municipalityCatalog[0].slug
|
||
expect(computeMunicipalityTerritorialClass(slug)).toBe(
|
||
computeMunicipalityTerritorialClass(slug),
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('computeAggregateTerritorialClass', () => {
|
||
const salvadorSlugs = municipalityCatalog
|
||
.filter((entry) => entry.kind === 'zona')
|
||
.map((entry) => entry.slug)
|
||
|
||
it('reads Salvador as one territory, not nineteen', () => {
|
||
expect(salvadorSlugs.length).toBe(19)
|
||
|
||
const aggregate = computeAggregateTerritorialClass(salvadorSlugs)
|
||
expect(aggregate.class).not.toBe('sem_base')
|
||
expect(aggregate.lq).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('sums the inputs instead of averaging the zones\u2019 ratios', () => {
|
||
const aggregate = computeAggregateTerritorialClass(salvadorSlugs)
|
||
const zoneLqs = salvadorSlugs
|
||
.map((slug) => computeMunicipalityTerritorialClass(slug).lq)
|
||
.filter((lq): lq is number => lq !== null)
|
||
const meanZoneLq = zoneLqs.reduce((total, lq) => total + lq, 0) / zoneLqs.length
|
||
|
||
// The aggregate is vote-weighted, so it must land inside the zones' range
|
||
// but not on their unweighted mean — LQ is a ratio, and the mean of ratios
|
||
// is not the ratio of the sums.
|
||
expect(aggregate.lq).toBeGreaterThanOrEqual(Math.min(...zoneLqs))
|
||
expect(aggregate.lq).toBeLessThanOrEqual(Math.max(...zoneLqs))
|
||
expect(aggregate.lq).not.toBeCloseTo(meanZoneLq, 6)
|
||
})
|
||
|
||
it('scales the field cut by the group size instead of a per-município median', () => {
|
||
// Field headroom is a LEVEL, so a summed one has to meet a summed cut.
|
||
// Comparing 19 zones' worth of headroom against the catalog median would
|
||
// clear it by an order of magnitude and make "marginal" unreachable for
|
||
// any group — the failure mode E12's 27-território rollup would inherit.
|
||
const perUnit = { ownVotes: 50, fieldHeadroom: 900, fieldHeadroomCut: 1_000 }
|
||
expect(classifyMunicipalityTerritory(input(perUnit)).class).toBe('marginal')
|
||
expect(
|
||
classifyMunicipalityTerritory(
|
||
input({ ...perUnit, fieldHeadroom: 900 * 3, fieldHeadroomCut: 1_000 * 3 }),
|
||
).class,
|
||
).toBe('marginal')
|
||
})
|
||
|
||
it('adds up the share of his statewide vote across the group', () => {
|
||
const aggregate = computeAggregateTerritorialClass(salvadorSlugs)
|
||
const summed = salvadorSlugs.reduce(
|
||
(total, slug) => total + computeMunicipalityTerritorialClass(slug).ownShare,
|
||
0,
|
||
)
|
||
|
||
expect(aggregate.ownShare).toBeCloseTo(summed, 10)
|
||
})
|
||
|
||
it('matches the single-slug classifier for a group of one', () => {
|
||
const slug = municipalityCatalog.find((entry) => entry.kind === 'municipio')!.slug
|
||
|
||
expect(computeAggregateTerritorialClass([slug])).toEqual(
|
||
computeMunicipalityTerritorialClass(slug),
|
||
)
|
||
})
|
||
|
||
it('has no class for an empty group', () => {
|
||
expect(computeAggregateTerritorialClass([]).class).toBe('sem_base')
|
||
})
|
||
})
|
||
|
||
describe('territorialClassSortWeight', () => {
|
||
it('ranks reduto first and leaves sem_base without a weight', () => {
|
||
expect(territorialClassSortWeight.reduto).toBeGreaterThan(
|
||
territorialClassSortWeight.expansao ?? 0,
|
||
)
|
||
expect(territorialClassSortWeight.expansao).toBeGreaterThan(
|
||
territorialClassSortWeight.manutencao ?? 0,
|
||
)
|
||
expect(territorialClassSortWeight.manutencao).toBeGreaterThan(
|
||
territorialClassSortWeight.marginal ?? 0,
|
||
)
|
||
expect(territorialClassSortWeight.sem_base).toBeNull()
|
||
})
|
||
})
|