teqo-backup/tests/int/speechCatalog.int.spec.ts
Francisco Solla aca3ea6188 feat(C153): catálogo de falas do Solla — schema, access, import idempotente e busca
Collections speech/speechSegment no grupo Comunicação, papel communicator
(leitura restrita) e índice trigram normalizado para a busca por segmento.
Import idempotente da Câmara (discurso→evento→trecho→VOD→ASR) com facetas
gazetteer+LLM, proveniência e preservação de curadoria manual; parser de
quem presidia; reusa a lógica pura do piloto C152 via scripts/lib.

Related #955
2026-09-13 02:08:55 -03:00

277 lines
8.1 KiB
TypeScript

// @vitest-environment node
import { randomUUID } from 'node:crypto'
import type { Payload, RequiredDataFromCollectionSlug } from 'payload'
import { getPayload } from 'payload'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { Speech } from '@/payload-types'
import config from '@/payload.config'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
let payload: Payload
const createdSpeechIds: number[] = []
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
const createSpeech = async (
overrides: Partial<RequiredDataFromCollectionSlug<'speech'>> = {},
): Promise<Speech> => {
const speech = await payload.create({
collection: 'speech',
data: {
sourceKey: `test-${randomUUID()}`,
speechAt: '2023-02-07T17:28',
type: 'BREVES COMUNICAÇÕES',
phase: 'Breves Comunicações',
summary: 'A defesa do SUS e da saúde pública baiana.',
officialTranscript: 'O SR. JORGE SOLLA (Bloco/PT - BA) - Sr. Presidente...',
keywords: ['Saúde', 'SUS'],
topics: ['saude'],
classifiedBy: 'gazetteer',
...overrides,
},
depth: 0,
overrideAccess: true,
})
createdSpeechIds.push(speech.id)
return speech
}
describe('speech catalog (C153)', () => {
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 by a cascade assertion
}
}
})
it('derives the year from the wall-clock speechAt', async () => {
const speech = await createSpeech()
expect(speech.year).toBe(2023)
})
it('normalizes the segment text into searchText and matches accent-insensitively', async () => {
const speech = await createSpeech()
const segment = await payload.create({
collection: 'speechSegment',
data: {
speech: speech.id,
order: 1,
startSeconds: 0,
endSeconds: 4.2,
text: 'A defesa do SUS e da saúde pública',
// Deliberately wrong: the beforeValidate hook owns this field.
searchText: 'errado',
},
depth: 0,
overrideAccess: true,
})
expect(segment.searchText).toBe('a defesa do sus e da saude publica')
const hits = await payload.find({
collection: 'speechSegment',
where: { searchText: { contains: 'saude' } },
depth: 0,
overrideAccess: true,
})
expect(hits.docs.map((doc) => doc.id)).toContain(segment.id)
})
it('deletes the speech segments with the speech', async () => {
const speech = await createSpeech()
const segment = await payload.create({
collection: 'speechSegment',
data: {
speech: speech.id,
order: 1,
startSeconds: 0,
endSeconds: 2,
text: 'Trecho qualquer',
searchText: '',
},
depth: 0,
overrideAccess: true,
})
await payload.delete({ collection: 'speech', id: speech.id, depth: 0, overrideAccess: true })
const remaining = await payload.find({
collection: 'speechSegment',
where: { id: { equals: segment.id } },
depth: 0,
overrideAccess: true,
})
expect(remaining.docs).toHaveLength(0)
})
it('reads for communicator, coordinator, candidate and admin; denies advisor, leader and anonymous', async () => {
const speech = await createSpeech()
const fixtures = campaignFixtures()
const communicator = await fixtures.createCampaignUser('communicator')
const coordinator = await fixtures.createCampaignUser('coordinator')
const candidate = await fixtures.createCampaignUser('candidate')
const advisor = await fixtures.createCampaignUser('advisor')
const leader = await fixtures.createCampaignUser('leader')
const admin = await fixtures.createAdminUser()
for (const reader of [communicator, coordinator, candidate, admin]) {
const found = await payload.find({
collection: 'speech',
where: { id: { equals: speech.id } },
depth: 0,
user: reader,
overrideAccess: false,
})
expect(found.docs).toHaveLength(1)
}
for (const denied of [advisor, leader, undefined]) {
await expect(
payload.find({
collection: 'speech',
where: { id: { equals: speech.id } },
depth: 0,
user: denied,
overrideAccess: false,
}),
).rejects.toThrow(/permissão/i)
}
})
it('updates facets for coordinator/candidate/admin but not for communicator/advisor', async () => {
const speech = await createSpeech()
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const candidate = await fixtures.createCampaignUser('candidate')
const communicator = await fixtures.createCampaignUser('communicator')
const advisor = await fixtures.createCampaignUser('advisor')
const admin = await fixtures.createAdminUser()
for (const editor of [coordinator, candidate, admin]) {
const updated = await payload.update({
collection: 'speech',
id: speech.id,
data: { topics: ['educacao'] },
depth: 0,
user: editor,
overrideAccess: false,
})
expect(updated.topics).toEqual(['educacao'])
}
for (const denied of [communicator, advisor]) {
await expect(
payload.update({
collection: 'speech',
id: speech.id,
data: { topics: ['cultura'] },
depth: 0,
user: denied,
overrideAccess: false,
}),
).rejects.toThrow(/permissão/i)
}
})
it('creates/deletes only for Payload admin', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const admin = await fixtures.createAdminUser()
await expect(
payload.create({
collection: 'speech',
data: {
sourceKey: `test-${randomUUID()}`,
speechAt: '2023-02-08T10:00',
classifiedBy: 'gazetteer',
},
depth: 0,
user: coordinator,
overrideAccess: false,
}),
).rejects.toThrow(/permissão/i)
const speech = await createSpeech()
await expect(
payload.delete({
collection: 'speech',
id: speech.id,
depth: 0,
user: coordinator,
overrideAccess: false,
}),
).rejects.toThrow(/permissão/i)
const adminCreated = await payload.create({
collection: 'speech',
data: {
sourceKey: `test-${randomUUID()}`,
speechAt: '2023-02-08T10:00',
classifiedBy: 'gazetteer',
},
depth: 0,
user: admin,
overrideAccess: false,
})
createdSpeechIds.push(adminCreated.id)
expect(adminCreated.id).toBeGreaterThan(0)
})
it('preserves manual facets on unauthenticated writes and lets admin curation through', async () => {
const speech = await createSpeech({ classifiedBy: 'manual', topics: ['saude'] })
await payload.update({
collection: 'speech',
id: speech.id,
data: { topics: ['cultura'], scopes: ['brasil'], classifiedBy: 'llm' },
depth: 0,
overrideAccess: true,
})
const afterImport = await payload.findByID({
collection: 'speech',
id: speech.id,
depth: 0,
overrideAccess: true,
})
expect(afterImport.topics).toEqual(['saude'])
expect(afterImport.scopes).toEqual([])
expect(afterImport.classifiedBy).toBe('manual')
const admin = await campaignFixtures().createAdminUser()
await payload.update({
collection: 'speech',
id: speech.id,
data: { topics: ['cultura'] },
depth: 0,
user: admin,
overrideAccess: false,
})
const curated = await payload.findByID({
collection: 'speech',
id: speech.id,
depth: 0,
overrideAccess: true,
})
expect(curated.topics).toEqual(['cultura'])
expect(curated.classifiedBy).toBe('manual')
})
})