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
69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { generateObjectMock, deepSeekMock } = vi.hoisted(() => ({
|
|
generateObjectMock: vi.fn(),
|
|
deepSeekMock: vi.fn(() => 'mock-model'),
|
|
}))
|
|
|
|
vi.mock('ai', () => ({ generateObject: generateObjectMock }))
|
|
vi.mock('@ai-sdk/deepseek', () => ({ deepSeek: deepSeekMock }))
|
|
|
|
import { expandSpeechSearchTheme } from '@/utilities/ai/expandSpeechSearchTheme'
|
|
|
|
beforeEach(() => {
|
|
process.env.DEEPSEEK_API_KEY = 'test-deepseek-key'
|
|
generateObjectMock.mockReset()
|
|
deepSeekMock.mockClear()
|
|
})
|
|
|
|
afterEach(() => {
|
|
delete process.env.DEEPSEEK_API_KEY
|
|
})
|
|
|
|
describe('expandSpeechSearchTheme (C192)', () => {
|
|
it('returns the sanitized terms the model produced', async () => {
|
|
generateObjectMock.mockResolvedValue({
|
|
object: { terms: ['SUS%', 'saúde pública', 'sus', 'ab'] },
|
|
})
|
|
|
|
await expect(expandSpeechSearchTheme('defesa do SUS')).resolves.toEqual({
|
|
terms: ['SUS', 'saúde pública'],
|
|
})
|
|
})
|
|
|
|
it('honra a lista vazia como resposta legítima (não é indisponibilidade)', async () => {
|
|
generateObjectMock.mockResolvedValue({ object: { terms: [] } })
|
|
|
|
await expect(expandSpeechSearchTheme('tema qualquer')).resolves.toEqual({ terms: [] })
|
|
})
|
|
|
|
it('curto-circuita sem chave sem chamar o provedor', async () => {
|
|
delete process.env.DEEPSEEK_API_KEY
|
|
|
|
await expect(expandSpeechSearchTheme('SUS')).resolves.toBeNull()
|
|
expect(generateObjectMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('curto-circuita um tema vazio', async () => {
|
|
await expect(expandSpeechSearchTheme(' ')).resolves.toBeNull()
|
|
expect(generateObjectMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('degrada para null em erro do provedor — nunca lança', async () => {
|
|
generateObjectMock.mockRejectedValue(new Error('provider down'))
|
|
|
|
await expect(expandSpeechSearchTheme('SUS')).resolves.toBeNull()
|
|
})
|
|
|
|
it('envia o tema e um abortSignal limitado ao modelo', async () => {
|
|
generateObjectMock.mockResolvedValue({ object: { terms: ['SUS'] } })
|
|
|
|
await expandSpeechSearchTheme('defesa do SUS')
|
|
|
|
const call = generateObjectMock.mock.calls[0]![0] as { prompt: string; abortSignal: unknown }
|
|
expect(call.prompt).toBe('defesa do SUS')
|
|
expect(call.abortSignal).toBeInstanceOf(AbortSignal)
|
|
})
|
|
})
|