teqo/tests/unit/goalCoverage.unit.spec.ts
Francisco Ignácio Fontoura Solla cf6ae6c47c Pass 2 W2: correcoes de fronteira lib/utilities e client boundary
- helpers puros movidos utilities->lib: slug, phone, wordStartFilter, voteEstimate (9 inversoes lib->utilities zeradas)
- chaves de consent em src/lib/campaignConsentKeys.ts (client-safe); campaignInvite/campaignConsent re-exportam; onda0ConsentTexts referencia as constantes
- 'server-only' em 21 loaders Payload/cache (CLI segue com --conditions=react-server)
- ciclo de tipos supporterListFilters <-> supporterUi quebrado (SupporterListFilterInput estrutural; sqlFilters idem)
- votePledgeData dividido: votePledgeViews (puro, client-safe) + votePledgeData (loaders, server-only) - padrao contract-module
- predicados de role client-safe em src/lib/campaignRoles.ts; access/shared deriva deles (fonte unica); nav.ts para de importar o barrel campaignAccess (casts 'as CampaignUser' eliminados)
2026-07-25 14:38:41 -03:00

124 lines
5.2 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.

import { describe, expect, it } from 'vitest'
import {
aggregateGoalCoverage,
computeGoalCoverage,
computeGoalCoverageByScenario,
formatGoalCoverageDeficitLabel,
} from '@/utilities/goalCoverage'
import type { SuggestedGoalByScenario } from '@/utilities/municipalityPotential'
import { createEmptyMunicipalityPledgeAggregate } from '@/utilities/votePledgeViews'
/** Suggested goal ladder shaped like `deriveSuggestedGoalsByScenario` output. */
const suggested = (
central: number,
{ pessimistic = central * 0.9, optimistic = central * 1.16 } = {},
): SuggestedGoalByScenario => ({ pessimistic, central, optimistic })
/**
* Fixes the meta×comprometido semantics locked in the E8 plan audit: a
* municipality can have `expectedVotes` filled by the E4R projection-sheet
* import with ZERO pledges behind it — coverage must report that gap, not
* 100% (which is what `resolveMunicipalityStaffVoteTotal`'s override
* behavior would give if reused here).
*/
describe('computeGoalCoverage', () => {
it('meta = expectedVotes[cenário] when set; comprometido = pledge aggregate only (never the override)', () => {
// Feira de Santana-style case: mesa expects 3000 (E4R sheet, cenário média),
// but only 1200 is backed by declared/estimated pledges.
const aggregate = createEmptyMunicipalityPledgeAggregate()
aggregate.effectiveByScenario.central = 1200
const coverage = computeGoalCoverage({ central: 3000 }, suggested(5000), aggregate, 'central')
expect(coverage.goal).toBe(3000) // NOT 100% — expectedVotes wins over suggestedGoal, not over pledges
expect(coverage.committed).toBe(1200)
expect(coverage.coverageRatio).toBeCloseTo(0.4, 6)
expect(coverage.deficit).toBe(1800)
})
it('falls back to suggestedGoal when expectedVotes is unset for that scenario', () => {
const aggregate = createEmptyMunicipalityPledgeAggregate()
aggregate.effectiveByScenario.central = 400
const coverage = computeGoalCoverage(null, suggested(1000), aggregate, 'central')
expect(coverage.goal).toBe(1000)
expect(coverage.committed).toBe(400)
expect(coverage.coverageRatio).toBeCloseTo(0.4, 6)
})
it('coverageRatio is null (not Infinity/NaN) when goal is zero', () => {
const aggregate = createEmptyMunicipalityPledgeAggregate()
const coverage = computeGoalCoverage(null, suggested(0), aggregate, 'central')
expect(coverage.coverageRatio).toBeNull()
expect(coverage.deficit).toBe(0)
})
it('deficit is negative when the municipality is over-committed relative to its goal', () => {
const aggregate = createEmptyMunicipalityPledgeAggregate()
aggregate.effectiveByScenario.central = 1500
const coverage = computeGoalCoverage(null, suggested(1000), aggregate, 'central')
expect(coverage.deficit).toBe(-500)
expect(coverage.coverageRatio).toBeCloseTo(1.5, 6)
})
it('reads each scenario independently (mirrors staffVoteTotalByScenario)', () => {
const aggregate = createEmptyMunicipalityPledgeAggregate()
aggregate.effectiveByScenario.pessimistic = 100
aggregate.effectiveByScenario.central = 200
aggregate.effectiveByScenario.optimistic = 300
const byScenario = computeGoalCoverageByScenario(
{ pessimistic: 500, optimistic: 900 },
// Suggested fallback used only for 'central' here (unset in expectedVotes).
suggested(600, { pessimistic: 540, optimistic: 700 }),
aggregate,
)
expect(byScenario.pessimistic.goal).toBe(500)
expect(byScenario.pessimistic.committed).toBe(100)
expect(byScenario.central.goal).toBe(600) // falls back to suggestedGoal
expect(byScenario.central.committed).toBe(200)
expect(byScenario.optimistic.goal).toBe(900)
expect(byScenario.optimistic.committed).toBe(300)
})
it('the suggested fallback moves with the scenario (E9: both sides are per-scenario)', () => {
const aggregate = createEmptyMunicipalityPledgeAggregate()
const byScenario = computeGoalCoverageByScenario(
null,
suggested(1000, { pessimistic: 900, optimistic: 1163 }),
aggregate,
)
expect(byScenario.pessimistic.goal).toBe(900)
expect(byScenario.central.goal).toBe(1000)
expect(byScenario.optimistic.goal).toBe(1163)
})
})
describe('goal coverage labels', () => {
it('renders a fractional decomposed goal as whole votes', () => {
// A suggested goal of 100.968 votes must not read as "100,968" in pt-BR.
const aggregate = createEmptyMunicipalityPledgeAggregate()
const coverage = computeGoalCoverage(null, suggested(100.968), aggregate, 'central')
expect(formatGoalCoverageDeficitLabel(coverage)).toBe('Faltam 101 votos para a meta')
})
})
describe('aggregateGoalCoverage', () => {
it('sums goal and committed across municipalities and recomputes the ratio', () => {
const aggregate = aggregateGoalCoverage([
{ goal: 3000, committed: 1200, coverageRatio: 0.4, deficit: 1800 },
{ goal: 1000, committed: 1000, coverageRatio: 1, deficit: 0 },
])
expect(aggregate.goal).toBe(4000)
expect(aggregate.committed).toBe(2200)
expect(aggregate.coverageRatio).toBeCloseTo(0.55, 6)
expect(aggregate.deficit).toBe(1800)
})
it('returns coverageRatio null for an empty list (zero goal)', () => {
expect(aggregateGoalCoverage([]).coverageRatio).toBeNull()
})
})