import { describe, expect, it } from 'vitest' import { BRIEFING_LABEL, BRIEFING_MINIMUMS, BRIEFING_QA_SIDES, briefingAnchorFact, normalizeBriefingContent, trimBriefing, } from '../../scripts/lib/briefingContent.mjs' import { buildDossierReport } from '../../scripts/lib/dossieBlocks.mjs' import { mergeDossierResearch, normalizeDossierResearchInput, } from '../../scripts/lib/dossieResearch.mjs' import { MUNICIPALITY_UNIT, THEME_UNIT } from '../../scripts/lib/dossieUnit.mjs' // C210 (replanejado 2026-09-23): the authored briefing may only anchor on the // dossiê ledger (facts with source) and is 100% recorte — princípios/crenças, // defesas, fatos-âncora, uma linha de plano e Q&A. These tests pin the // fail-closed validation, the deny-list and the deterministic shed order that // keeps the four-sheet PDF honest. const generatedAt = new Date('2026-09-22T12:00:00.000Z') const snapshot = { meta: { readAt: '2026-09-16T10:00:00.000Z', database: 'teqo_test' }, municipality: { slug: 'ilheus', name: 'Ilhéus', region: 'Litoral Sul', ibgeCode: '2913606' }, } const research = mergeDossierResearch([ normalizeDossierResearchInput({ municipalitySlug: 'ilheus', era: 'B', researchedAt: '2026-09-16T10:00:00.000Z', items: [ { id: 'era_b_equipamentos', answer: 'Ambulância e atenção domiciliar no município', brief: { title: 'Ambulância e atenção domiciliar', note: 'Registro de 2012.' }, sourceUrl: 'https://saude.test/ambulancia', sourceDate: '2012-01-25', }, { id: 'era_b_programas', answer: 'Saúde em Movimento com menção direta', sourceUrl: 'https://saude.test/programas', sourceDate: '2013-05-10', }, ], news: [], gaps: [], }), normalizeDossierResearchInput({ municipalitySlug: 'ilheus', era: 'C', researchedAt: '2026-09-16T10:00:00.000Z', items: [ { id: 'era_c_atuacao', answer: 'Articulação regional pela UFBA', sourceUrl: 'https://camara.test/ufba', sourceDate: '2023-09-22', }, ], news: [], gaps: [], }), ]) const report = buildDossierReport({ snapshot, research, generatedAt }) const facts = report.bulletinFacts const validRaw = (overrides: Record = {}) => ({ unitId: 'municipality', municipalitySlug: 'ilheus', generatedAt: generatedAt.toISOString(), subtitle: 'Litoral Sul · consulta antes e durante o contato', lede: 'Universidade pública não é gasto: é projeto de país.', defenses: [ { factId: 'era_c_atuacao', title: 'Ensino superior', note: 'Defesa regional.' }, { factId: 'era_b_programas', title: 'Saúde da Família', note: 'Defesa local.' }, ], essential: [ { factId: 'era_b_equipamentos', title: 'Ambulância', note: 'Registro local de 2012.' }, { factId: 'era_b_programas', title: 'Saúde em Movimento', note: 'Menção direta.' }, { gapReason: 'sem fala própria localizada', title: 'Sem fala local', note: 'Anote a lacuna.' }, ], plan: 'Combine onde, quando e como votar e registre o compromisso nomeado.', qa: [ { side: 'direita', question: 'Não voto no PT.', acknowledge: 'Entendo que a escolha pesa.', answer: 'Há registro nominal na saúde.', close: 'Posso contar com o 1313?', factId: 'era_b_equipamentos', }, { side: 'esquerda', question: 'Por que ainda falta tanto?', acknowledge: 'A cobrança é legítima.', answer: 'O levantamento registra lacunas.', close: 'Posso contar com o 1313?', gapReason: 'sem registro localizado', }, { side: 'entrega', question: 'Ele prometeu o quê?', acknowledge: 'Promessa precisa estar registrada.', answer: 'Não foi localizada promessa nominal.', close: 'Pelo trabalho comprovado, vota 1313?', gapReason: 'sem promessa nominal', }, { side: 'direita', question: 'E o governo de Jerônimo?', acknowledge: 'É justo cobrar o que chegou.', answer: 'O briefing registra outros períodos.', close: 'Posso pedir seu voto 1313?', gapReason: 'sem entrega do governo atual', }, { side: 'esquerda', question: 'Por que falar de Jacobina?', acknowledge: 'Você tem razão em separar cidade e região.', answer: 'As iniciativas aparecem como regionais.', close: 'Posso contar com o 1313?', factId: 'era_c_atuacao', }, ], ...overrides, }) const normalize = (raw: Record = validRaw()) => normalizeBriefingContent(raw, { unit: MUNICIPALITY_UNIT, slug: 'ilheus', facts }) describe('normalizeBriefingContent', () => { it('resolves every anchor against the sourced ledger and keeps the lacuna explicit', () => { const content = normalize() expect(content.essential).toHaveLength(3) expect(content.essential[0].fact?.sourceUrl).toBe('https://saude.test/ambulancia') expect(content.essential[2].gapReason).toBe('sem fala própria localizada') expect(content.defenses).toHaveLength(2) expect(content.defenses[0].fact?.sourceUrl).toBe('https://camara.test/ufba') expect(content.qa).toHaveLength(5) expect(content.plan).toMatch(/compromisso nomeado/) expect(content.warnings).toEqual([]) }) it('rejects a unitId or slug from another recorte', () => { expect(() => normalize(validRaw({ unitId: 'theme' }))).toThrow(/unitId/) expect(() => normalize(validRaw({ municipalitySlug: 'itabuna' }))).toThrow(/não é "ilheus"/) }) it('requires generatedAt, a lede and the one-line plan', () => { expect(() => normalize(validRaw({ generatedAt: 'ontem' }))).toThrow(/generatedAt inválido/) expect(() => normalize(validRaw({ lede: '' }))).toThrow(/"lede" obrigatório/) expect(() => normalize(validRaw({ plan: '' }))).toThrow(/"plan" obrigatório/) }) it('demands exactly one of factId | gapReason per item', () => { const both = validRaw() const essential = (both.essential as Record[]).map((item, index) => index === 0 ? { ...item, gapReason: 'também lacuna' } : item, ) expect(() => normalize({ ...both, essential })).toThrow( /exatamente um de "factId" \| "gapReason"/, ) const neither = (both.essential as Record[]).map((item, index) => index === 0 ? { title: 'x', note: 'y' } : item, ) expect(() => normalize({ ...both, essential: neither })).toThrow(/exatamente um/) }) it('fails closed when the factId does not resolve in a sourced fact', () => { const raw = validRaw() const essential = (raw.essential as Record[]).map((item, index) => index === 0 ? { ...item, factId: 'era_z_inexistente' } : item, ) expect(() => normalize({ ...raw, essential })).toThrow(/não resolve em fato com fonte/) expect(() => normalizeBriefingContent(validRaw(), { unit: MUNICIPALITY_UNIT, slug: 'ilheus', facts: [{ id: 'era_b_equipamentos' }], }), ).toThrow(/não resolve em fato com fonte/) }) it('blocks staff-only/scenario keys recursively', () => { expect(() => normalize(validRaw({ scenario: { estimatedVotes: 1234 } }))).toThrow( /cenário\/estimativa é staff-only/, ) expect(() => normalize( validRaw({ essential: [{ factId: 'era_b_equipamentos', title: 'x', note: 'y', estimatedVotes: 10 }], }), ), ).toThrow(/proibida/) }) it('covers both sides and enforces the list minimums', () => { const raw = validRaw() expect(() => normalize({ ...raw, qa: (raw.qa as Record[]).map((item) => item.side === 'esquerda' ? { ...item, side: 'direita' } : item, ), }), ).toThrow(/dois lados/) expect(() => normalize({ ...raw, qa: [] })).toThrow(/qa: mínimo de 4/) expect(() => normalize({ ...raw, essential: [] })).toThrow(/essential: mínimo de 3/) }) it('reports over-cap text as a warning, never as a hard failure', () => { const raw = validRaw() const content = normalize({ ...raw, lede: 'a'.repeat(500), plan: 'b'.repeat(300) }) expect(content.warnings.join('\n')).toMatch(/lede: 500 chars/) expect(content.warnings.join('\n')).toMatch(/plan: 300 chars/) }) it('resolves anchors through the unit slugField (theme seam)', () => { const themeFacts = ['era_b_equipamentos', 'era_b_programas', 'era_c_atuacao'].map((id) => ({ id, sourceUrl: `https://x.test/${id}`, sourceDate: '2011-08-30', })) const raw = validRaw({ unitId: 'theme' }) expect(() => normalizeBriefingContent(raw, { unit: THEME_UNIT, slug: 'educacao', facts: themeFacts }), ).toThrow(/themeSlug/) const content = normalizeBriefingContent( { ...raw, themeSlug: 'educacao' }, { unit: THEME_UNIT, slug: 'educacao', facts: themeFacts }, ) expect(content.slug).toBe('educacao') expect(THEME_UNIT.briefingNoun).toBe('tema') }) it('refuses the acervo sample (sourcePanel) as an anchor', () => { const panelFacts = [ { id: 'fala-1', headline: 'Pronunciamento', sourceUrl: 'https://x.test/fala', sourcePanel: true, }, ] expect(briefingAnchorFact(panelFacts, 'fala-1')).toBeNull() expect(() => normalizeBriefingContent(validRaw(), { unit: MUNICIPALITY_UNIT, slug: 'ilheus', facts: panelFacts, }), ).toThrow(/não resolve em fato com fonte/) }) it('resolves a repeated id through the item sourceUrl and fails closed on a wrong pair', () => { const duplicated = [ { id: 'era_b_equipamentos', sourceUrl: 'https://saude.test/ambulancia-2', sourceDate: '2019-01-01', }, ...facts, ] const raw = validRaw({ essential: [ { factId: 'era_b_equipamentos', sourceUrl: 'https://saude.test/ambulancia-2', title: 'Ambulância nova', note: 'Registro de 2019.', }, { factId: 'era_b_programas', title: 'Saúde em Movimento', note: 'Menção direta.' }, { gapReason: 'sem fala própria localizada', title: 'Sem fala local', note: 'Anote a lacuna.', }, ], }) const content = normalizeBriefingContent(raw, { unit: MUNICIPALITY_UNIT, slug: 'ilheus', facts: duplicated, }) expect(content.essential[0].fact?.sourceDate).toBe('2019-01-01') const wrongPair = (raw.essential as Record[]).map((item, index) => index === 0 ? { ...item, sourceUrl: 'https://saude.test/nao-existe' } : item, ) expect(() => normalizeBriefingContent( { ...raw, essential: wrongPair }, { unit: MUNICIPALITY_UNIT, slug: 'ilheus', facts: duplicated }, ), ).toThrow(/não resolve em fato com fonte/) }) it('reports qa caps with the real field names, once each', () => { const raw = validRaw() const qa = (raw.qa as Record[]).map((item, index) => index === 0 ? { ...item, question: 'q'.repeat(300), answer: 'a'.repeat(700) } : item, ) const content = normalize({ ...raw, qa }) expect(content.warnings.join('\n')).toContain('qa[0].question: 300 chars') expect(content.warnings.join('\n')).toContain('qa[0].answer: 700 chars') expect(content.warnings).toHaveLength(2) }) it('keeps the label literal and the qa sides pinned for the renderer', () => { expect(BRIEFING_LABEL).toBe('Insumo interno de capacitação — não publicar') expect(BRIEFING_QA_SIDES).toEqual(['direita', 'esquerda', 'entrega']) }) }) describe('briefingAnchorFact', () => { it('returns the ledger fact or null', () => { expect(briefingAnchorFact(facts, 'era_b_programas')?.sourceUrl).toBe( 'https://saude.test/programas', ) expect(briefingAnchorFact(facts, 'não-existe')).toBeNull() }) it('disambiguates a repeated id by the exact sourceUrl', () => { const duplicated = [ { id: 'era_b_programas', sourceUrl: 'https://saude.test/programas', sourceDate: '2013-05-10', }, { id: 'era_b_programas', sourceUrl: 'https://saude.test/programas-2', sourceDate: '2020-01-01', }, ] expect(briefingAnchorFact(duplicated, 'era_b_programas')?.sourceDate).toBe('2013-05-10') expect( briefingAnchorFact(duplicated, 'era_b_programas', 'https://saude.test/programas-2') ?.sourceDate, ).toBe('2020-01-01') expect(briefingAnchorFact(duplicated, 'era_b_programas', 'https://nope.test')).toBeNull() }) }) describe('trimBriefing (deterministic shed order)', () => { const content = normalize() it('drops qa first, then defenses, then stops', () => { const first = trimBriefing(content)! expect(first.qa).toHaveLength(BRIEFING_MINIMUMS.qa) expect(first.shed).toEqual({ qa: 1, defenses: 0 }) const second = trimBriefing(first)! expect(second.defenses).toHaveLength(1) expect(second.shed).toEqual({ qa: 1, defenses: 1 }) const third = trimBriefing(second)! expect(third.defenses).toHaveLength(0) expect(third.shed).toEqual({ qa: 1, defenses: 2 }) expect(trimBriefing(third)).toBeNull() }) it('never silences a required side when shedding qa', () => { const base = normalize() const qa = [ { ...base.qa[1], side: 'esquerda' }, { ...base.qa[2], side: 'esquerda' }, { ...base.qa[3], side: 'entrega' }, { ...base.qa[4], side: 'esquerda' }, { ...base.qa[0], side: 'direita' }, ] const trimmed = trimBriefing({ ...base, qa })! expect(trimmed.qa).toHaveLength(BRIEFING_MINIMUMS.qa) expect(trimmed.shed.qa).toBe(1) const sides = new Set(trimmed.qa.map((item: { side: string }) => item.side)) expect(sides.has('direita')).toBe(true) expect(sides.has('esquerda')).toBe(true) // The unique "direita" (last item) survives — the shed picked an "esquerda". expect(trimmed.qa.at(-1)?.side).toBe('direita') }) it('never touches the essential, the principles copy or the plan', () => { let current = content let next = trimBriefing(current) let guard = 0 while (next && guard < 50) { current = next next = trimBriefing(current) guard += 1 } expect(current.qa).toHaveLength(BRIEFING_MINIMUMS.qa) expect(current.defenses).toHaveLength(0) expect(current.essential).toEqual(content.essential) expect(current.lede).toBe(content.lede) expect(current.plan).toBe(content.plan) expect(next).toBeNull() }) })