mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Modo "Por tema" no acervo de falas: expansão determinística do tema via DeepSeek (generateObject, timeout, fallback null) alimenta o motor lexical existente (searchText/keywords OR), mantendo facetas AND, paginação por fala, o OR de cortes do C174 e o access control intacto. Resultado de tema mostra selo e "Por que apareceu" com trecho real (frase contínua), sem score; degradação honesta para termo exato com aviso e vazio honesto com saídas. - URL: mode=tema (default omitido; canonicaliza sem q), seletor otimista - ai/expandSpeechSearchTheme + lib/speechThemeTerms (sanitiza saída do modelo) - view model com themeMatchTerm + espelho speechMatchesSearchTerm - UI do card de tema (selos, proveniência, ações) e aviso degradado - sem migration/Consent/exposição pública; gate de leitura inalterado - testes unit/int/e2e; design hi-fi estendido e certificado pelo designer
153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { SPEECH_SCOPES, SPEECH_TOPICS } from '@/lib/speechFacets'
|
|
import {
|
|
buildSpeechFiltersKey,
|
|
buildSpeechListHref,
|
|
parseSpeechListParams,
|
|
resolveSpeechListUrl,
|
|
serializeCanonicalSpeechListSearchParams,
|
|
} from '@/utilities/speech/speechListUrl'
|
|
|
|
describe('parseSpeechListParams', () => {
|
|
it('parses the canonical facets and keeps the page', () => {
|
|
const state = parseSpeechListParams({
|
|
q: 'Farmácia Popular',
|
|
year: ['2026', '2025'],
|
|
topic: ['saude', 'educacao'],
|
|
scope: ['bahia'],
|
|
phase: ['Breves Comunicações'],
|
|
municipality: ['12', '999999'],
|
|
duration: ['curta', 'longa'],
|
|
page: '3',
|
|
})
|
|
|
|
expect(state).toEqual({
|
|
page: 3,
|
|
q: 'Farmácia Popular',
|
|
years: [2026, 2025],
|
|
topics: ['saude', 'educacao'],
|
|
scopes: ['bahia'],
|
|
phases: ['Breves Comunicações'],
|
|
municipalities: [12, 999999],
|
|
durations: ['curta', 'longa'],
|
|
})
|
|
})
|
|
|
|
it('collapses an exhaustive enum to "no filter"', () => {
|
|
const state = parseSpeechListParams({
|
|
topic: SPEECH_TOPICS.map(({ value }) => value),
|
|
scope: SPEECH_SCOPES.map(({ value }) => value),
|
|
})
|
|
expect(state.topics).toBeUndefined()
|
|
expect(state.scopes).toBeUndefined()
|
|
})
|
|
|
|
it('validates data-driven params structurally', () => {
|
|
const state = parseSpeechListParams({
|
|
year: ['99', '2026', '0000'],
|
|
municipality: ['0', '-4', '7'],
|
|
phase: ['x'.repeat(81), 'Plenário'],
|
|
q: 'a',
|
|
page: '0',
|
|
})
|
|
|
|
expect(state.years).toEqual([2026])
|
|
expect(state.municipalities).toEqual([7])
|
|
expect(state.phases).toEqual(['Plenário'])
|
|
// One-character search is not ready — the same rule as contact search.
|
|
expect(state.q).toBeUndefined()
|
|
expect(state.page).toBe(1)
|
|
})
|
|
|
|
it('drops unknown enum tokens', () => {
|
|
const state = parseSpeechListParams({ topic: 'inexistente', duration: 'maratona' })
|
|
expect(state.topics).toBeUndefined()
|
|
expect(state.durations).toBeUndefined()
|
|
})
|
|
|
|
it('parses the theme mode and ignores any other value (C192)', () => {
|
|
expect(parseSpeechListParams({ q: 'SUS', mode: 'tema' }).mode).toBe('tema')
|
|
expect(parseSpeechListParams({ q: 'SUS', mode: 'termo' }).mode).toBeUndefined()
|
|
expect(parseSpeechListParams({ q: 'SUS', mode: 'exato' }).mode).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('serializeCanonicalSpeechListSearchParams', () => {
|
|
it('canonicalizes through parse and omits defaults', () => {
|
|
const params = serializeCanonicalSpeechListSearchParams(
|
|
parseSpeechListParams({ topic: ['saude'], page: '1' }),
|
|
)
|
|
expect(params.toString()).toBe('topic=saude')
|
|
})
|
|
|
|
it('keeps every value of a multi-select facet', () => {
|
|
const params = serializeCanonicalSpeechListSearchParams(
|
|
parseSpeechListParams({ topic: ['saude', 'educacao'], duration: ['curta'] }),
|
|
)
|
|
expect(params.getAll('topic')).toEqual(['saude', 'educacao'])
|
|
expect(params.getAll('duration')).toEqual(['curta'])
|
|
})
|
|
|
|
it('serializes mode=tema only alongside a query (C192)', () => {
|
|
expect(
|
|
serializeCanonicalSpeechListSearchParams(
|
|
parseSpeechListParams({ q: 'SUS', mode: 'tema' }),
|
|
).toString(),
|
|
).toBe('q=SUS&mode=tema')
|
|
// A theme without a query canonicalizes away.
|
|
expect(
|
|
serializeCanonicalSpeechListSearchParams(parseSpeechListParams({ mode: 'tema' })).toString(),
|
|
).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('buildSpeechListHref', () => {
|
|
it('points at the acervo and clamps the page into the query', () => {
|
|
expect(buildSpeechListHref(parseSpeechListParams({ q: 'SUS' }), 2)).toBe(
|
|
'/campanha/comunicacao/acervo?q=SUS&page=2',
|
|
)
|
|
expect(buildSpeechListHref(parseSpeechListParams({}), 1)).toBe('/campanha/comunicacao/acervo')
|
|
})
|
|
|
|
it('keeps the exact-search deep link byte-identical (C192)', () => {
|
|
expect(buildSpeechListHref(parseSpeechListParams({ q: 'SUS' }), 1)).toBe(
|
|
'/campanha/comunicacao/acervo?q=SUS',
|
|
)
|
|
expect(buildSpeechListHref(parseSpeechListParams({ q: 'SUS', mode: 'tema' }), 1)).toBe(
|
|
'/campanha/comunicacao/acervo?q=SUS&mode=tema',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('resolveSpeechListUrl', () => {
|
|
it('canonicalizes unsupported params into a redirect', () => {
|
|
const resolved = resolveSpeechListUrl({ q: 'saude', desconhecido: 'x', page: '2' })
|
|
expect(resolved.state.q).toBe('saude')
|
|
expect(resolved.redirectHref).toBe('/campanha/comunicacao/acervo?q=saude&page=2')
|
|
})
|
|
|
|
it('clamps a page beyond the total', () => {
|
|
const resolved = resolveSpeechListUrl({ q: 'saude', page: '9' }, 2)
|
|
expect(resolved.state.page).toBe(2)
|
|
expect(resolved.redirectHref).toContain('page=2')
|
|
})
|
|
|
|
it('canonicalizes a theme mode without a query into a redirect (C192)', () => {
|
|
const withQuery = resolveSpeechListUrl({ q: 'SUS', mode: 'tema' })
|
|
expect(withQuery.state.mode).toBe('tema')
|
|
expect(withQuery.redirectHref).toBeUndefined()
|
|
|
|
const empty = resolveSpeechListUrl({ mode: 'tema' })
|
|
expect(empty.redirectHref).toBe('/campanha/comunicacao/acervo')
|
|
})
|
|
})
|
|
|
|
describe('buildSpeechFiltersKey', () => {
|
|
it('is empty without filters and stable with them', () => {
|
|
expect(buildSpeechFiltersKey(parseSpeechListParams({}))).toBe('')
|
|
expect(buildSpeechFiltersKey(parseSpeechListParams({ topic: ['saude'] }))).toBe('topic=saude')
|
|
})
|
|
})
|