teqo-backup/tests/int/themeSnapshot.int.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

124 lines
3.8 KiB
TypeScript

// @vitest-environment node
import { randomUUID } from 'node:crypto'
import type { Payload } from 'payload'
import { getPayload } from 'payload'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import config from '@/payload.config'
import { composeThemeSnapshot } from '../../scripts/themeSnapshot.mjs'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
// C190: the theme/area snapshot recorte IS the acervo tag (`Speech.topics`), so
// there is no nominal bridge. Pins the read projection (no PII), the topic
// filter and the explicit gap when the recorte is empty.
let payload: Payload
const createdSpeechIds: number[] = []
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
const identity = {
slug: 'educacao',
value: 'educacao',
label: 'Educação',
taxonomyNote: 'Taxonomia do acervo',
}
describe('theme snapshot (C190)', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
afterAll(async () => {
for (const id of createdSpeechIds) {
try {
await payload.delete({ collection: 'speech', id, depth: 0, overrideAccess: true })
} catch {
// already deleted
}
}
})
it(
'projects the topic-matched speeches without PII and records the recorte',
{
timeout: 30_000,
},
async () => {
const fixtures = campaignFixtures()
const candidate = await fixtures.createCampaignUser('candidate')
const speech = await payload.create({
collection: 'speech',
data: {
sourceKey: `test-theme-${randomUUID()}`,
speechAt: '2024-05-10T17:28',
type: 'DISCURSO',
phase: 'Discurso',
summary: 'Defesa do financiamento da educação básica.',
officialTranscript: 'O SR. JORGE SOLLA (Bloco/PT - BA) - Sr. Presidente...',
keywords: ['escola', 'educação'],
topics: ['educacao'],
classifiedBy: 'gazetteer',
},
depth: 0,
overrideAccess: true,
})
createdSpeechIds.push(speech.id)
const actor = { ...candidate, collection: 'campaignUser' as const }
const snapshot = await composeThemeSnapshot({
payload,
actor,
identity,
readAt: '2026-09-18T11:00:00.000Z',
codeSha: 'test-sha',
database: 'localhost:5432/teqo_test',
})
expect(snapshot.meta.kind).toBe('theme')
expect(snapshot.theme).toMatchObject({
slug: 'educacao',
value: 'educacao',
label: 'Educação',
})
expect(snapshot.theme.topics).toEqual(['educacao'])
expect(snapshot.speeches.rows.some((row: { id: number }) => row.id === speech.id)).toBe(true)
const row = snapshot.speeches.rows.find(
(candidateRow: { id: number }) => candidateRow.id === speech.id,
)!
expect('officialTranscript' in row).toBe(false)
expect('mentionExcerpt' in row).toBe(false)
expect(JSON.stringify(snapshot)).not.toContain('@')
},
)
it('declares an explicit gap when the theme has no speech', async () => {
const fixtures = campaignFixtures()
const candidate = await fixtures.createCampaignUser('candidate')
const actor = { ...candidate, collection: 'campaignUser' as const }
const snapshot = await composeThemeSnapshot({
payload,
actor,
identity: {
slug: 'pessoa-deficiencia',
value: 'pessoa-deficiencia',
label: 'Pessoa com Deficiência',
taxonomyNote: 'Taxonomia do acervo',
},
readAt: '2026-09-18T11:00:00.000Z',
})
expect(snapshot.speeches.rows).toEqual([])
expect(snapshot.speeches.totalCount).toBe(0)
expect(snapshot.gaps).toContainEqual(expect.objectContaining({ id: 'acervo_sem_falas' }))
})
})