Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
185 lines
5.7 KiB
TypeScript
185 lines
5.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ErrorCode } from '@d3ro/core/errors'
|
|
import type { History, MeetingSessionRow } from '../../../src/main/db/schema'
|
|
|
|
interface MockBuilderOptions {
|
|
get?: unknown
|
|
all?: unknown[]
|
|
error?: Error
|
|
}
|
|
|
|
function createMockBuilder(options: MockBuilderOptions = {}) {
|
|
const builder = {
|
|
from: vi.fn(),
|
|
where: vi.fn(),
|
|
orderBy: vi.fn(),
|
|
limit: vi.fn(),
|
|
get: vi.fn(),
|
|
all: vi.fn(),
|
|
}
|
|
builder.from.mockImplementation(() => {
|
|
if (options.error !== undefined) throw options.error
|
|
return builder
|
|
})
|
|
builder.where.mockReturnValue(builder)
|
|
builder.orderBy.mockReturnValue(builder)
|
|
builder.limit.mockReturnValue(builder)
|
|
builder.get.mockReturnValue(options.get ?? null)
|
|
builder.all.mockReturnValue(options.all ?? [])
|
|
return builder
|
|
}
|
|
|
|
const selectQueue: ReturnType<typeof createMockBuilder>[] = []
|
|
const mockDb = {
|
|
select: vi.fn(() => selectQueue.shift() ?? createMockBuilder()),
|
|
}
|
|
|
|
vi.mock('../../../src/main/db', () => ({
|
|
getDatabase: () => mockDb,
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/LoggerService', () => ({
|
|
getLogger: () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn(),
|
|
}),
|
|
}))
|
|
|
|
const record: History = {
|
|
id: 'record-1',
|
|
title: '제품 메모',
|
|
originalText: '초기 원문',
|
|
polishedText: '다음 분기 출시 계획을 정리했습니다.',
|
|
focusedApp: null,
|
|
focusedAppName: null,
|
|
focusedAppWindowTitle: null,
|
|
mode: 'dictation',
|
|
status: 'completed',
|
|
errorCode: null,
|
|
audioLocalPath: null,
|
|
duration: 10,
|
|
detectedLanguage: 'ko',
|
|
micDevice: null,
|
|
wordCount: 6,
|
|
sttModel: null,
|
|
llmModel: null,
|
|
sttLatencyMs: null,
|
|
llmLatencyMs: null,
|
|
createdAt: 100,
|
|
updatedAt: 100,
|
|
appVersion: '1.9.0',
|
|
summaryText: null,
|
|
isFavorite: false,
|
|
}
|
|
|
|
const meeting: MeetingSessionRow = {
|
|
id: 'meeting-1',
|
|
title: '주간 회의',
|
|
status: 'completed',
|
|
startedAt: 200,
|
|
endedAt: 300,
|
|
durationMs: 100,
|
|
rawTranscript: '출시 계획을 다음 주에 검토합니다.',
|
|
editedTranscript: null,
|
|
minutesMarkdown: null,
|
|
minutesJson: null,
|
|
sttModel: null,
|
|
llmModel: null,
|
|
sttLatencyMs: null,
|
|
llmLatencyMs: null,
|
|
errorMessage: null,
|
|
createdAt: 200,
|
|
updatedAt: 300,
|
|
}
|
|
|
|
describe('UnifiedSearchService', () => {
|
|
beforeEach(() => {
|
|
selectQueue.length = 0
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('normalizes input and creates a bounded excerpt around the match', async () => {
|
|
const { createUnifiedSearchExcerpt, normalizeUnifiedSearchQuery } = await import(
|
|
'../../../src/main/services/UnifiedSearchService'
|
|
)
|
|
expect(normalizeUnifiedSearchQuery(' 출시\n 계획 ')).toBe('출시 계획')
|
|
expect(normalizeUnifiedSearchQuery('a'.repeat(250))).toHaveLength(200)
|
|
expect(normalizeUnifiedSearchQuery(` ${'a'.repeat(199)} b`)).toBe('a'.repeat(199))
|
|
const excerpt = createUnifiedSearchExcerpt(`앞부분 ${'x'.repeat(80)} 출시 계획 ${'y'.repeat(80)}`, '출시 계획', 60)
|
|
expect(excerpt).toContain('출시 계획')
|
|
expect(excerpt.startsWith('…')).toBe(true)
|
|
expect(excerpt.endsWith('…')).toBe(true)
|
|
})
|
|
|
|
it('merges records and meeting transcripts by occurrence time and preserves source ids', async () => {
|
|
selectQueue.push(
|
|
createMockBuilder({ get: { count: 1 } }),
|
|
createMockBuilder({ all: [record] }),
|
|
createMockBuilder({ get: { count: 1 } }),
|
|
createMockBuilder({ all: [meeting] }),
|
|
)
|
|
const { getUnifiedSearchService, resetUnifiedSearchServiceForTests } = await import(
|
|
'../../../src/main/services/UnifiedSearchService'
|
|
)
|
|
resetUnifiedSearchServiceForTests()
|
|
|
|
const response = getUnifiedSearchService().search({ query: '계획', limit: 10 })
|
|
|
|
expect(response.total).toBe(2)
|
|
expect(response.results.map(({ source, id }) => ({ source, id }))).toEqual([
|
|
{ source: 'meeting', id: 'meeting-1' },
|
|
{ source: 'record', id: 'record-1' },
|
|
])
|
|
expect(response.results[0]).toMatchObject({
|
|
matchedField: 'transcript',
|
|
excerpt: '출시 계획을 다음 주에 검토합니다.',
|
|
})
|
|
expect(response.results[1]).toMatchObject({ matchedField: 'content' })
|
|
})
|
|
|
|
it('returns immediately for an empty query', async () => {
|
|
const { getUnifiedSearchService, resetUnifiedSearchServiceForTests } = await import(
|
|
'../../../src/main/services/UnifiedSearchService'
|
|
)
|
|
resetUnifiedSearchServiceForTests()
|
|
expect(getUnifiedSearchService().search({ query: ' ' })).toEqual({
|
|
query: '',
|
|
results: [],
|
|
total: 0,
|
|
})
|
|
expect(mockDb.select).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it.each([
|
|
null,
|
|
undefined,
|
|
{},
|
|
{ query: 42 },
|
|
{ query: '계획', limit: Number.NaN },
|
|
{ query: '계획', limit: Number.POSITIVE_INFINITY },
|
|
{ query: '계획', limit: '10' },
|
|
])('rejects malformed runtime parameters with ConfigInvalidValue: %o', async (params) => {
|
|
const { getUnifiedSearchService, resetUnifiedSearchServiceForTests } = await import(
|
|
'../../../src/main/services/UnifiedSearchService'
|
|
)
|
|
resetUnifiedSearchServiceForTests()
|
|
|
|
expect(() => getUnifiedSearchService().search(params)).toThrowError(
|
|
expect.objectContaining({ code: ErrorCode.ConfigInvalidValue }),
|
|
)
|
|
expect(mockDb.select).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('maps database failures to the IPC error contract', async () => {
|
|
selectQueue.push(createMockBuilder({ error: new Error('database unavailable') }))
|
|
const { getUnifiedSearchService, resetUnifiedSearchServiceForTests } = await import(
|
|
'../../../src/main/services/UnifiedSearchService'
|
|
)
|
|
resetUnifiedSearchServiceForTests()
|
|
expect(() => getUnifiedSearchService().search({ query: '계획' })).toThrowError(
|
|
expect.objectContaining({ code: ErrorCode.DBQueryFailed }),
|
|
)
|
|
})
|
|
})
|