mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 02:23:07 +00:00
329 lines
10 KiB
TypeScript
329 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { buildCityReport } from '../../scripts/lib/cityReportBlocks.mjs'
|
|
import {
|
|
normalizeResearchInput,
|
|
RESEARCH_CHECKLIST_IDS,
|
|
} from '../../scripts/lib/cityReportResearch.mjs'
|
|
|
|
const generatedAt = new Date('2026-09-15T12:00:00.000Z')
|
|
|
|
const validItem = (id: string) => ({
|
|
id,
|
|
answer: `Resposta de ${id}`,
|
|
sourceUrl: `https://exemplo.test/${id}`,
|
|
sourceDate: '2026-09-10',
|
|
})
|
|
|
|
const research = normalizeResearchInput(
|
|
{
|
|
municipalitySlug: 'feira-de-santana',
|
|
researchedAt: '2026-09-14T10:00:00.000Z',
|
|
items: RESEARCH_CHECKLIST_IDS.map(validItem),
|
|
news: [
|
|
{
|
|
title: 'Matéria local',
|
|
url: 'https://jornal.test/materia',
|
|
publishedAt: '2026-09-01T00:00:00.000Z',
|
|
outlet: 'Jornal Local',
|
|
},
|
|
],
|
|
gaps: [],
|
|
},
|
|
{ now: generatedAt },
|
|
)
|
|
|
|
const emendas = {
|
|
status: 'ok',
|
|
reason: null,
|
|
detail: null,
|
|
sourceUrl: 'https://api.portaldatransparencia.gov.br/api-de-dados/emendas?nomeAutor=JORGE+SOLLA',
|
|
consultedAt: '2026-09-15T11:00:00.000Z',
|
|
requestCount: 4,
|
|
years: [2023, 2024, 2025, 2026],
|
|
authorName: 'JORGE SOLLA',
|
|
authorCode: '1234',
|
|
rows: [
|
|
{
|
|
code: '202312340001',
|
|
year: 2023,
|
|
type: 'Individual',
|
|
authorCode: '1234',
|
|
empenhado: 1_000_000,
|
|
liquidado: 500_000,
|
|
pago: 250_000,
|
|
restoPago: 10_000,
|
|
},
|
|
],
|
|
totals: { empenhado: 1_000_000, liquidado: 500_000, pago: 250_000, restoPago: 10_000 },
|
|
}
|
|
|
|
const snapshot = {
|
|
meta: {
|
|
source: 'Base Teqo (produção, leitura read-only)',
|
|
readAt: '2026-09-15T10:30:00.000Z',
|
|
readOnly: true,
|
|
database: '127.0.0.1:5433/teqo_1313',
|
|
codeSha: 'abc1234',
|
|
actorId: 3,
|
|
actorName: 'Seed Candidato',
|
|
actorRole: 'candidate',
|
|
actorRoleLabel: 'Candidato',
|
|
},
|
|
municipality: {
|
|
id: 42,
|
|
slug: 'feira-de-santana',
|
|
name: 'Feira de Santana',
|
|
kind: 'municipio',
|
|
city: 'Feira de Santana',
|
|
region: 'Portal do Sertão',
|
|
ibgeCode: '2910800',
|
|
tseCityCode: '31270',
|
|
zoneNumber: null,
|
|
tseZones: [12, 13],
|
|
lastUpdateAt: '2026-09-01T10:00:00.000Z',
|
|
},
|
|
electoral: {
|
|
candidateName: 'Jorge Solla',
|
|
candidateParty: 'PT',
|
|
series: [
|
|
{ year: 2014, votes: 30_000 },
|
|
{ year: 2018, votes: 40_000 },
|
|
{ year: 2022, votes: 50_000 },
|
|
],
|
|
tally2022: { aptos: 400_000, comparecimento: 300_000, votosValidos: 280_000 },
|
|
ticket2022: { president: 100_000, governor: 90_000 },
|
|
rank2022: { rank: 3, votes: 50_000, share: 0.05, totalUnits: 435 },
|
|
},
|
|
goal: {
|
|
suggestedGoal: 60_000,
|
|
goalCoverage: { goal: 60_000, committed: 15_000, coverageRatio: 0.25, deficit: 45_000 },
|
|
territorialClass: {
|
|
class: 'expansao',
|
|
factors: [],
|
|
lq: 1.2,
|
|
ownShare: 0.05,
|
|
inCoreBlock: true,
|
|
},
|
|
potential: {},
|
|
territoryCaptureBenchmark: {},
|
|
},
|
|
pledges: {
|
|
declaredTotal: 15_000,
|
|
effectiveByScenario: { pessimistic: 10_000, central: 15_000, optimistic: 20_000 },
|
|
pledgeCount: 7,
|
|
missingEstimateCount: 1,
|
|
lastPledgeAt: '2026-08-30T10:00:00.000Z',
|
|
},
|
|
leaderships: {
|
|
totalCount: 9,
|
|
rows: [
|
|
{
|
|
id: 1,
|
|
name: 'Liderança Um',
|
|
supportStatus: 'engajado',
|
|
exclusive: true,
|
|
organizationNames: ['Associação X'],
|
|
stateDeputies: [{ name: 'Deputada Estadual' }],
|
|
hasAppAccess: true,
|
|
updatedAt: '2026-09-10T10:00:00.000Z',
|
|
},
|
|
],
|
|
},
|
|
advisors: [{ id: 2, name: 'Seed Assessor' }],
|
|
signals: {
|
|
totalCount: 1,
|
|
rows: [
|
|
{
|
|
id: 10,
|
|
createdAt: '2026-09-12T10:00:00.000Z',
|
|
body: 'Sinal recente sobre a cidade',
|
|
polarity: 'boa',
|
|
urgent: false,
|
|
adversarySignal: false,
|
|
responsibleName: 'Seed Assessor',
|
|
activeVolunteers: 10,
|
|
newSupports: 2,
|
|
resolvedAt: null,
|
|
},
|
|
],
|
|
},
|
|
activities: {
|
|
upcoming: [
|
|
{
|
|
id: 20,
|
|
title: 'Visita ao centro',
|
|
status: 'confirmado',
|
|
startAt: '2026-09-20T13:00:00.000Z',
|
|
endAt: null,
|
|
locality: 'Centro',
|
|
deputyPresent: true,
|
|
taskProgress: { done: 1, total: 2 },
|
|
},
|
|
],
|
|
recent: [],
|
|
},
|
|
conjuncture: {
|
|
priority: 'alta',
|
|
expectedVotes: { pessimistic: 40_000, central: 50_000, optimistic: 70_000 },
|
|
politicalTrend: {
|
|
status: 'favoravel',
|
|
note: 'Prefeito sinaliza apoio',
|
|
recordedByName: 'Seed Coordenador',
|
|
recordedAt: '2026-08-20T10:00:00.000Z',
|
|
},
|
|
engagementLevel: 'n3',
|
|
levelNote: 'Rede ativa',
|
|
levelChangedAt: '2026-08-01T10:00:00.000Z',
|
|
strengths: ['Rede de saúde forte'],
|
|
risks: ['Oposição com rádio local'],
|
|
dobradinhaNotes: 'Dobradinha com a deputada estadual',
|
|
nextSteps: 'Agendar reunião com o prefeito',
|
|
budgetNotes: 'Emenda de custeio aportada em 2025',
|
|
stateDeputies: [{ id: 5, name: 'Deputada Estadual', party: 'PT' }],
|
|
},
|
|
speeches: {
|
|
totalCount: 1,
|
|
rows: [
|
|
{
|
|
id: 30,
|
|
speechAt: '2026-07-01T10:00:00.000Z',
|
|
year: 2026,
|
|
phase: 'Discurso em plenário',
|
|
summary: 'Resumo da fala',
|
|
officialTextUrl: 'https://camara.test/discurso',
|
|
watchHref: '/campanha/comunicacao/acervo/30?t=120',
|
|
excerpt: 'Trecho da fala sobre Feira de Santana',
|
|
},
|
|
],
|
|
},
|
|
demands: {
|
|
totalCount: 1,
|
|
rows: [
|
|
{
|
|
id: 40,
|
|
title: 'Demanda de saúde',
|
|
kind: 'saude',
|
|
status: 'recebida',
|
|
createdAt: '2026-09-01T10:00:00.000Z',
|
|
updatedAt: '2026-09-05T10:00:00.000Z',
|
|
},
|
|
],
|
|
},
|
|
demographics: {
|
|
population: 600_000,
|
|
ageBands: { '0-17': 150_000, '18-29': 120_000, '30-59': 250_000, '60+': 80_000 },
|
|
sexShareFemale: 0.52,
|
|
medianAge: 34,
|
|
},
|
|
}
|
|
|
|
type ReportItem = { label?: string; text?: string; value?: string; hint?: string | null }
|
|
type ReportRow = { label: string; value: string; source?: { url?: string } }
|
|
type ReportBlock = {
|
|
kind: string
|
|
title?: string
|
|
items?: ReportItem[]
|
|
rows?: ReportRow[]
|
|
cells?: Array<{ title: string; blocks: ReportBlock[] }>
|
|
body?: string[]
|
|
sources?: Array<{ kind: string; url?: string | null }>
|
|
left?: { title: string; items: ReportItem[] }
|
|
right?: { title: string; items: ReportItem[] }
|
|
}
|
|
type ReportShape = {
|
|
page1: { blocks: ReportBlock[] }
|
|
sections: Array<{ id: string; title: string; blocks: ReportBlock[] }>
|
|
}
|
|
|
|
const asReport = (value: unknown) => value as ReportShape
|
|
|
|
describe('buildCityReport', () => {
|
|
const report = asReport(buildCityReport({ snapshot, research, emendas, generatedAt }))
|
|
|
|
it('builds the 10 deepening sections in the product order', () => {
|
|
expect(report.sections).toHaveLength(10)
|
|
expect(report.sections[0].title).toMatch(/Conta eleitoral completa/)
|
|
expect(report.sections[9].title).toMatch(/Fontes e limites/)
|
|
})
|
|
|
|
it('keeps page 1 in the contract order (six blocks + footer)', () => {
|
|
const kinds = report.page1.blocks
|
|
.map((block) => block.kind)
|
|
.filter((kind) => kind !== 'callout')
|
|
expect(kinds).toEqual(['strip', 'grid', 'kpis', 'pair', 'cards', 'footerNote'])
|
|
const titles = report.page1.blocks.map((block) => block.title).filter(Boolean)
|
|
expect(titles).toEqual(
|
|
expect.arrayContaining(['O que Solla entregou', 'Riscos — oposição/disputa local']),
|
|
)
|
|
const grid = report.page1.blocks.find((block) => block.kind === 'grid')!
|
|
expect(grid.cells!.map((cell) => cell.title)).toEqual(['Conta eleitoral 2022', 'Quem é quem'])
|
|
const pair = report.page1.blocks.find((block) => block.kind === 'pair')!
|
|
expect(pair.left!.title).toBe('O que anunciar agora')
|
|
expect(pair.right!.items.some((item) => item.text?.includes('Empenho'))).toBe(true)
|
|
})
|
|
|
|
it('reads the relative lens (share/rank), never the statewide absolute', () => {
|
|
const grid = report.page1.blocks.find((block) => block.kind === 'grid')!
|
|
const kpis = grid.cells![0].blocks.find((block) => block.kind === 'kpis')!
|
|
expect(kpis.items![0].value).toBe('50.000')
|
|
expect(kpis.items![0].hint).toContain('5,0%')
|
|
expect(kpis.items![1].value).toBe('3º de 435')
|
|
})
|
|
|
|
it('shows emendas with phase and the official source', () => {
|
|
const delivered = report.page1.blocks.find(
|
|
(block) => block.kind === 'kpis' && block.title === 'O que Solla entregou',
|
|
)!
|
|
expect(delivered.items![0].value).toBe('R$ 1,0 mi')
|
|
expect(delivered.items![0].hint).toContain('Pago: R$ 250 mil')
|
|
expect(delivered.sources![0]).toEqual(
|
|
expect.objectContaining({
|
|
kind: 'official',
|
|
url: expect.stringContaining('portaldatransparencia'),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('degrades missing emendas into an explicit gap, never a zero', () => {
|
|
const gapReport = asReport(
|
|
buildCityReport({
|
|
snapshot,
|
|
research,
|
|
emendas: {
|
|
status: 'gap',
|
|
reason: 'Chave ausente',
|
|
detail: null,
|
|
sourceUrl: null,
|
|
consultedAt: null,
|
|
},
|
|
generatedAt,
|
|
}),
|
|
)
|
|
const delivered = gapReport.page1.blocks.find(
|
|
(block) => block.kind === 'kpis' && block.title === 'O que Solla entregou',
|
|
)!
|
|
expect(delivered.items![0].value).toBe('Sem fonte oficial')
|
|
const gaps = gapReport.page1.blocks.find(
|
|
(block) => block.kind === 'callout' && block.title === 'Pontos sem leitura',
|
|
)!
|
|
expect(gaps.body!.join(' ')).toMatch(/Emendas/)
|
|
})
|
|
|
|
it('puts research answers in Quem é quem with their source', () => {
|
|
const grid = report.page1.blocks.find((block) => block.kind === 'grid')!
|
|
const who = grid.cells![1].blocks[0]
|
|
const prefeito = who.rows!.find((row) => row.label === 'Prefeito(a)')!
|
|
expect(prefeito.value).toBe('Resposta de prefeito')
|
|
expect(prefeito.source!.url).toBe('https://exemplo.test/prefeito')
|
|
})
|
|
|
|
it('builds the region panorama from the committed artifact', () => {
|
|
const region = report.sections.find((section) => section.id === 'regiao')!.blocks[0]
|
|
const rowByLabel = (label: string) => region.rows!.find((row) => row.label === label)!
|
|
expect(rowByLabel('Território').value).toBe('Portal do Sertão')
|
|
expect(rowByLabel('Municípios no TI').value).toBe('17')
|
|
const position = Number(rowByLabel('Posição do município no TI').value.split('º')[0])
|
|
expect(position).toBeGreaterThan(0)
|
|
})
|
|
})
|