teqo-backup/tests/unit/speechFacets.unit.spec.ts
Francisco Solla e5e1ae7ee8 feat(C190): dossiê Solla por tema/área + boletim modelo (skill + builder)
Terceira variante da família de dossiês: recorte pela área canônica do
acervo (SPEECH_TOPICS), com o pipeline C186/C187 generalizado para a forma
subject via THEME_UNIT (generalização aditiva; C186/C187 byte-idênticos).

- resolveSpeechTopic no dono speechFacets (fail-closed, sem slug inventado)
- themeSnapshot + extract-theme-snapshot (THEME_REPORT_CONFIRM=1, read-only)
- build-dossie-solla-tema (pack medido, PDF A4 + .md + boletim 1 página)
- skill/comando/2 subagentes (researcher por era + redator) + specs
- cenas theme-only no descritor (acervo universo/amostra, abrangência,
  lacunas, hierarquia de fontes, pills do boletim) certificadas pelo designer

Closes #1143
2026-09-18 17:02:56 -03:00

209 lines
7.6 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from 'vitest'
import {
mergeFacetClassification,
parseLlmFacetResponse,
resolveSpeechTopic,
} from '@/lib/speechFacets'
import { classifySpeechByGazetteer, matchMunicipalityMentions } from '@/lib/speechGazetteer'
describe('resolveSpeechTopic (C190)', () => {
it('resolves the canonical value and the pt-BR label (accent/case insensitive)', () => {
expect(resolveSpeechTopic('educacao')?.value).toBe('educacao')
expect(resolveSpeechTopic('Educação')?.value).toBe('educacao')
expect(resolveSpeechTopic(' EDUCAÇÃO ')?.value).toBe('educacao')
expect(resolveSpeechTopic('Seguranca Publica')?.value).toBe('seguranca-publica')
expect(resolveSpeechTopic('Direitos Humanos e Assistência Social')?.value).toBe(
'direitos-humanos',
)
})
it('fails closed on an unknown or empty token (never an invented slug)', () => {
expect(resolveSpeechTopic('educacao-infantil')).toBeNull()
expect(resolveSpeechTopic('')).toBeNull()
expect(resolveSpeechTopic(' ')).toBeNull()
})
})
describe('classifySpeechByGazetteer', () => {
it('detects topics, explicit scopes and municipality mentions', () => {
const result = classifySpeechByGazetteer({
summary: 'Investimentos em saúde e educação na Bahia.',
keywords: ['SUS', 'Escola'],
transcript: 'Defendemos o SUS, a escola pública e a universidade em Feira de Santana.',
})
expect(result.topics).toContain('saude')
expect(result.topics).toContain('educacao')
expect(result.scopes).toContain('bahia')
expect(result.municipalities.map((entry) => entry.slug)).toContain('feira-de-santana')
expect(result.people).toEqual([])
expect(result.programs).toEqual([])
expect(result.projects).toEqual([])
})
it('matches whole words only (no "sus" inside "suspeito")', () => {
const result = classifySpeechByGazetteer({
transcript: 'O suspeito foi ouvido e liberado.',
})
expect(result.topics).not.toContain('saude')
})
it('infers the Bahia scope from a municipality mention alone', () => {
const result = classifySpeechByGazetteer({
transcript: 'A cidade de Vitória da Conquista recebeu o investimento.',
})
expect(result.scopes).toEqual(['bahia'])
})
it('ignores official keywords for place mentions (controlled topic vocabulary)', () => {
const result = classifySpeechByGazetteer({
summary: 'Debate sobre financiamento da educação.',
keywords: ['Saúde', 'Educação', 'Governo federal'],
transcript: 'Precisamos investir na educação pública.',
})
expect(result.topics).toContain('saude')
expect(result.municipalities).toEqual([])
const withPlace = classifySpeechByGazetteer({
summary: 'Investimento em Saúde, no interior da Bahia.',
transcript: 'A comitiva esteve em Saúde.',
})
expect(withPlace.municipalities.map((entry) => entry.slug)).toEqual(['saude'])
})
})
describe('matchMunicipalityMentions', () => {
it('consumes the longest name (São Félix do Coribe never also yields Coribe)', () => {
const slugs = matchMunicipalityMentions('Ele passou por São Félix do Coribe.').map(
(entry) => entry.slug,
)
expect(slugs).toEqual(['sao-felix-do-coribe'])
})
it('does not split a longer name into a shorter one (Condeúba is not Conde)', () => {
const slugs = matchMunicipalityMentions('A comitiva esteve em Condeúba.').map(
(entry) => entry.slug,
)
expect(slugs).toEqual(['condeuba'])
})
it('requires a capitalized proper noun (prose "saúde" is not the municipality Saúde)', () => {
expect(matchMunicipalityMentions('A saúde pública é prioridade.')).toEqual([])
expect(
matchMunicipalityMentions('A agenda passou em Saúde, no interior da Bahia.').map(
(entry) => entry.slug,
),
).toEqual(['saude'])
})
it('requires a place context for ambiguous names (Saúde, Wagner, Santana)', () => {
expect(matchMunicipalityMentions('A visita da Ministra da Saúde foi destaque.')).toEqual([])
expect(matchMunicipalityMentions('O senador Jaques Wagner discursou.')).toEqual([])
expect(matchMunicipalityMentions('O deputado Santana votou com o governo.')).toEqual([])
expect(
matchMunicipalityMentions('Ele esteve em Saúde, no interior.').map((entry) => entry.slug),
).toEqual(['saude'])
expect(
matchMunicipalityMentions('A comitiva passou em Wagner.').map((entry) => entry.slug),
).toEqual(['wagner'])
})
it('ignores institutional phrases that contain a municipality name', () => {
expect(matchMunicipalityMentions('O Sistema Único de Saúde atende a população.')).toEqual([])
expect(matchMunicipalityMentions('O Banco Central elevou os juros.')).toEqual([])
expect(matchMunicipalityMentions('O Palácio do Planalto confirmou a agenda.')).toEqual([])
expect(
matchMunicipalityMentions('Ele esteve em Central, no interior da Bahia.').map(
(entry) => entry.slug,
),
).toEqual(['central'])
})
it('maps a Salvador mention to its 19 zone entries', () => {
const slugs = matchMunicipalityMentions('O comício foi em Salvador.').map((entry) => entry.slug)
expect(slugs).toHaveLength(19)
expect(slugs.every((slug) => slug.startsWith('salvador-ze-'))).toBe(true)
})
})
describe('parseLlmFacetResponse', () => {
it('maps labels and values to the taxonomy, dropping unknown values', () => {
expect(
parseLlmFacetResponse({
topics: ['Saúde', 'seguranca-publica', 'inexistente'],
scopes: ['Bahia', 'Mundo'],
people: ['Dilma Rousseff', 'dilma rousseff', ''],
programs: ['Minha Casa, Minha Vida'],
projects: ['PEC 45'],
}),
).toEqual({
topics: ['saude', 'seguranca-publica'],
scopes: ['bahia'],
people: ['Dilma Rousseff'],
programs: ['Minha Casa, Minha Vida'],
projects: ['PEC 45'],
})
})
it('maps the long labels too', () => {
const parsed = parseLlmFacetResponse({
topics: ['Direitos Humanos e Assistência Social', 'Infraestrutura e Transporte'],
})
expect(parsed?.topics).toEqual(['direitos-humanos', 'infraestrutura'])
})
it('returns null for a response without any known key', () => {
expect(parseLlmFacetResponse(null)).toBeNull()
expect(parseLlmFacetResponse('texto')).toBeNull()
expect(parseLlmFacetResponse({ foo: 1 })).toBeNull()
})
it('caps and truncates the free-text mentions', () => {
const parsed = parseLlmFacetResponse({
people: Array.from({ length: 20 }, (_, index) => `Pessoa ${index}`),
programs: ['x'.repeat(200)],
})
expect(parsed?.people).toHaveLength(12)
expect(parsed?.programs[0]).toHaveLength(120)
})
})
describe('mergeFacetClassification', () => {
const gazetteer = classifySpeechByGazetteer({
transcript: 'O SUS e a escola pública em Feira de Santana.',
})
it('unions topics and prefers the validated LLM scopes', () => {
const merged = mergeFacetClassification(gazetteer, {
topics: ['cultura'],
scopes: ['brasil'],
people: ['Lula'],
programs: [],
projects: [],
})
expect(merged.topics).toEqual([...gazetteer.topics, 'cultura'])
expect(merged.scopes).toEqual(['brasil'])
expect(merged.people).toEqual(['Lula'])
expect(merged.classifiedBy).toBe('llm')
})
it('keeps the gazetteer provenance without an LLM response', () => {
const merged = mergeFacetClassification(gazetteer, null)
expect(merged.classifiedBy).toBe('gazetteer')
expect(merged.scopes).toEqual(gazetteer.scopes)
expect(merged.municipalities).toEqual(gazetteer.municipalities)
})
})