feat: add mobile shell and unified search
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
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
This commit is contained in:
parent
f505a03d34
commit
5ca3709b32
98 changed files with 5168 additions and 2093 deletions
185
apps/desktop/tests/main/services/UnifiedSearchService.test.ts
Normal file
185
apps/desktop/tests/main/services/UnifiedSearchService.test.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
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 }),
|
||||
)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue