mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Some checks failed
Deploy / preflight (push) Has been cancelled
Deploy / verify (push) Has been cancelled
Deploy / deploy staging (teqo-staging) (push) Has been cancelled
Deploy / requeue when main advanced (push) Has been cancelled
Deploy / deploy production (teqo-1313) (push) Has been cancelled
A federal foi decidida em 04/10; o documento de cidade passa a servir inteiro à eleição de Lula. Saem conta eleitoral/cadeira de Solla, concorrentes e pré-candidatos, emendas e entregas (sem fetch do Portal da Transparência no builder), acervo de falas, panorama regional e sinais/demandas; demografia, economia e transporte ficam como contexto da visita. A página 1 vira só mobilização: KPIs oficiais do artefato TSE 2026 (Lula/Δ, X₁/X₂, Solla), papel determinístico da cidade, quem mobiliza, movimento local do 2º turno, riscos e o painel "O que pedir e falar" (pedido Lula 13; guardrail de votos em jogo no lugar do defeso). O checklist cai de 10 para 8 itens (emendas_web e quem_investe saem; relacao_campo/disputa_local reancorados no 2º turno); listas preCandidates/investments/emendasIndicators saem do contrato (arquivos antigos seguem aceitos em replay). O snapshot enxuga para rede + conjuntura + demografia (cityReportTerritory.mjs removido). Prova: unit 5.232 verdes, int do snapshot sem PII e sem os grupos aposentados, lint/format/typecheck/knip limpos e rebuild da fixture de Cruz das Almas (9 seções, 16 páginas contra 21, página 1 fechando sem fallback).
269 lines
9.3 KiB
TypeScript
269 lines
9.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
normalizeResearchInput,
|
|
RESEARCH_CHECKLIST_IDS,
|
|
} from '../../scripts/lib/cityReportResearch.mjs'
|
|
|
|
const now = new Date('2026-09-15T12:00:00.000Z')
|
|
|
|
const validItem = (id: string, overrides: Record<string, unknown> = {}) => ({
|
|
id,
|
|
answer: `Resposta de ${id}`,
|
|
sourceUrl: `https://exemplo.test/${id}`,
|
|
sourceDate: '2026-09-10',
|
|
...overrides,
|
|
})
|
|
|
|
const baseResearch = (overrides: Record<string, unknown> = {}) => ({
|
|
municipalitySlug: 'teixeira-de-freitas',
|
|
researchedAt: '2026-09-14T10:00:00.000Z',
|
|
items: RESEARCH_CHECKLIST_IDS.map((id) => validItem(id)),
|
|
news: [],
|
|
gaps: [],
|
|
...overrides,
|
|
})
|
|
|
|
describe('normalizeResearchInput', () => {
|
|
it('pins the 2º-turno mobilization checklist (no mandate-era items)', () => {
|
|
expect(RESEARCH_CHECKLIST_IDS).toContain('segundo_turno')
|
|
expect(RESEARCH_CHECKLIST_IDS).toContain('relacao_campo')
|
|
expect(RESEARCH_CHECKLIST_IDS).not.toContain('emendas_web')
|
|
expect(RESEARCH_CHECKLIST_IDS).not.toContain('quem_investe')
|
|
expect(RESEARCH_CHECKLIST_IDS).toHaveLength(8)
|
|
})
|
|
|
|
it('keeps fully sourced items', () => {
|
|
const research = normalizeResearchInput(baseResearch(), { now })
|
|
expect(research.items).toHaveLength(RESEARCH_CHECKLIST_IDS.length)
|
|
expect(research.gaps).toEqual([])
|
|
})
|
|
|
|
it('ignores retired top-level lists from old research files (replay compat)', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
preCandidates: [{ name: 'X', office: 'Deputado federal' }],
|
|
investments: [{ topic: 'Obra', detail: 'sem fonte' }],
|
|
emendasIndicators: [{ author: 'Y', sphere: 'municipio' }],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect('preCandidates' in research).toBe(false)
|
|
expect('investments' in research).toBe(false)
|
|
expect('emendasIndicators' in research).toBe(false)
|
|
expect(research.gaps).toEqual([])
|
|
})
|
|
|
|
it('normalizes the optional per-item summary without changing the source contract (C188)', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
items: RESEARCH_CHECKLIST_IDS.map((id) =>
|
|
id === 'prefeito'
|
|
? validItem(id, { summary: ' Prefeito X, do PSD, em primeiro mandato. ' })
|
|
: id === 'vice'
|
|
? validItem(id, { summary: ' ' })
|
|
: validItem(id),
|
|
),
|
|
}),
|
|
{ now },
|
|
)
|
|
const prefeito = research.items.find((item) => item.id === 'prefeito')!
|
|
expect(prefeito.summary).toBe('Prefeito X, do PSD, em primeiro mandato.')
|
|
expect(prefeito.answer).toBe('Resposta de prefeito')
|
|
expect(research.items.find((item) => item.id === 'vice')!.summary).toBeNull()
|
|
expect(research.items.find((item) => item.id === 'vereadores')!.summary).toBeNull()
|
|
expect(research.gaps).toEqual([])
|
|
})
|
|
|
|
it('throws when the research is not dated', () => {
|
|
expect(() => normalizeResearchInput(baseResearch({ researchedAt: null }), { now })).toThrow(
|
|
/researchedAt/,
|
|
)
|
|
})
|
|
|
|
it('throws when the slug is missing', () => {
|
|
expect(() => normalizeResearchInput(baseResearch({ municipalitySlug: '' }), { now })).toThrow(
|
|
/municipalitySlug/,
|
|
)
|
|
})
|
|
|
|
it('degrades an item without source to an explicit gap', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
items: [
|
|
validItem('prefeito', { sourceUrl: null }),
|
|
...RESEARCH_CHECKLIST_IDS.slice(1).map((id) => validItem(id)),
|
|
],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect(research.items.find((item) => item.id === 'prefeito')).toBeUndefined()
|
|
expect(research.gaps).toContainEqual(
|
|
expect.objectContaining({ id: 'prefeito', reason: expect.stringMatching(/Sem fonte/) }),
|
|
)
|
|
})
|
|
|
|
it('marks checklist items never researched as gaps', () => {
|
|
const research = normalizeResearchInput(baseResearch({ items: [validItem('prefeito')] }), {
|
|
now,
|
|
})
|
|
expect(research.gaps.filter((gap) => gap.reason === 'Não pesquisado.')).toHaveLength(
|
|
RESEARCH_CHECKLIST_IDS.length - 1,
|
|
)
|
|
})
|
|
|
|
it('treats an explicit gap as researched, without a duplicate "Não pesquisado"', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
items: RESEARCH_CHECKLIST_IDS.filter((id) => id !== 'imprensa_local').map((id) =>
|
|
validItem(id),
|
|
),
|
|
gaps: [{ id: 'imprensa_local', label: 'Imprensa local', reason: 'Sem veículo ativo.' }],
|
|
}),
|
|
{ now },
|
|
)
|
|
const pressGaps = research.gaps.filter((gap) => gap.id === 'imprensa_local')
|
|
expect(pressGaps).toHaveLength(1)
|
|
expect(pressGaps[0].reason).toBe('Sem veículo ativo.')
|
|
expect(research.gaps.filter((gap) => gap.reason === 'Não pesquisado.')).toHaveLength(0)
|
|
})
|
|
|
|
it('keeps item extra sources with URL + date and drops ones without', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
items: [
|
|
validItem('prefeito', {
|
|
extraSources: [
|
|
{ label: 'Polo regional', url: 'https://exemplo.test/polo', date: '2026-09-01' },
|
|
{ label: 'Sem data', url: 'https://exemplo.test/sem-data' },
|
|
],
|
|
}),
|
|
...RESEARCH_CHECKLIST_IDS.filter((id) => id !== 'prefeito').map((id) => validItem(id)),
|
|
],
|
|
}),
|
|
{ now },
|
|
)
|
|
const item = research.items.find((entry) => entry.id === 'prefeito')!
|
|
expect(item.extraSources).toHaveLength(1)
|
|
expect(item.extraSources[0].url).toBe('https://exemplo.test/polo')
|
|
expect(research.gaps).toContainEqual(
|
|
expect.objectContaining({ id: 'fonte_extra', reason: expect.stringMatching(/URL ou data/) }),
|
|
)
|
|
})
|
|
|
|
it('keeps sourced approach suggestions and gaps ones without source', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
approach: [
|
|
{
|
|
persona: 'Coordenação',
|
|
topic: 'Saúde',
|
|
suggestion: 'Agenda no hospital regional.',
|
|
sourceUrl: 'https://exemplo.test/saude',
|
|
sourceDate: '2026-09-01',
|
|
},
|
|
{
|
|
persona: 'Ciência política',
|
|
topic: 'Sem fonte',
|
|
suggestion: 'Não entra.',
|
|
sourceUrl: null,
|
|
sourceDate: null,
|
|
},
|
|
],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect(research.approach).toHaveLength(1)
|
|
expect(research.approach[0].persona).toBe('Coordenação')
|
|
expect(research.gaps).toContainEqual(
|
|
expect.objectContaining({
|
|
id: 'abordagem_sem_fonte',
|
|
reason: expect.stringMatching(/URL e data/),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('drops news outside the 90-day window and without source', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
news: [
|
|
{
|
|
title: 'Matéria válida',
|
|
url: 'https://jornal.test/materia',
|
|
publishedAt: '2026-09-01T00:00:00.000Z',
|
|
outlet: 'Jornal Local',
|
|
},
|
|
{
|
|
title: 'Velha',
|
|
url: 'https://jornal.test/velha',
|
|
publishedAt: '2026-01-01T00:00:00.000Z',
|
|
},
|
|
{ title: 'Sem URL', publishedAt: '2026-09-01T00:00:00.000Z' },
|
|
],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect(research.news).toHaveLength(1)
|
|
expect(research.news[0].title).toBe('Matéria válida')
|
|
expect(research.gaps).toContainEqual(expect.objectContaining({ id: 'noticia_fora_da_janela' }))
|
|
expect(research.gaps).toContainEqual(expect.objectContaining({ id: 'noticia_sem_fonte' }))
|
|
})
|
|
|
|
it('rejects unknown checklist ids', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
items: [...RESEARCH_CHECKLIST_IDS.map((id) => validItem(id)), validItem('inventado')],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect(research.items.find((item) => item.id === 'inventado')).toBeUndefined()
|
|
expect(research.gaps).toContainEqual(
|
|
expect.objectContaining({ id: 'inventado', reason: expect.stringMatching(/checklist/) }),
|
|
)
|
|
})
|
|
|
|
it('keeps sourced opposition facts and gaps entries without source', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
opposition: [
|
|
{
|
|
topic: 'Obra parada no campo adversário',
|
|
detail: 'Anúncio sem entrega.',
|
|
sourceUrl: 'https://exemplo.test/oposicao',
|
|
sourceDate: '2026-09-10',
|
|
},
|
|
{ topic: 'Sem fonte', detail: 'Não entra.' },
|
|
],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect(research.opposition).toHaveLength(1)
|
|
const [firstOpposition] = research.opposition as unknown as Array<{ topic: string }>
|
|
expect(firstOpposition.topic).toBe('Obra parada no campo adversário')
|
|
expect(research.gaps).toContainEqual(
|
|
expect.objectContaining({
|
|
id: 'opposition_sem_fonte',
|
|
reason: expect.stringMatching(/URL e data/),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('keeps sourced dobradinhas and gaps those without source', () => {
|
|
const research = normalizeResearchInput(
|
|
baseResearch({
|
|
alliances: [
|
|
{
|
|
topic: 'Dobradinha estadual',
|
|
detail: 'Deputada X (PT) indicada com Solla.',
|
|
sourceUrl: 'https://exemplo.test/dobradinha',
|
|
sourceDate: '2026-09-10',
|
|
},
|
|
{ topic: 'Sem fonte', detail: 'Não entra.' },
|
|
],
|
|
}),
|
|
{ now },
|
|
)
|
|
expect(research.alliances).toHaveLength(1)
|
|
expect(research.gaps).toContainEqual(expect.objectContaining({ id: 'alliances_sem_fonte' }))
|
|
})
|
|
})
|