d3ro-voice/apps/desktop/tests/red/history.usecase.test.ts
Yun Chan 708e20f747
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
2026-08-20 11:12:05 +09:00

208 lines
9.3 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { ErrorCode } from '@d3ro/core/errors'
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
import { getHistoryService } from '../../src/main/services/HistoryService'
import { registerHistoryHandlers } from '../../src/main/ipc/history-handlers'
import { historyInput, invokeIpc, useRedHarness } from './harness'
import { USER_TEXT } from './fixtures'
useRedHarness()
describe('유스케이스: 히스토리 CRUD / 검색 / 통계 / IPC', () => {
it('대시보드에서 빈 히스토리를 열면 0건이다', () => {
const page = getHistoryService().list({ page: 0, pageSize: 20 })
expect(page.total).toBe(0)
expect(page.entries).toEqual([])
expect(page.totalPages).toBe(0)
})
it('받아쓰기 완료 후 히스토리에 원문이 저장된다', () => {
const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.KO }))
expect(entry.id).toMatch(/^[0-9a-f-]{36}$/i)
expect(entry.originalText).toBe(USER_TEXT.KO)
expect(entry.status).toBe('completed')
expect(getHistoryService().getById(entry.id)?.originalText).toBe(USER_TEXT.KO)
})
it('영문 받아쓰기도 같은 경로로 저장된다', () => {
const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.EN }))
expect(getHistoryService().getById(entry.id)?.originalText).toBe(USER_TEXT.EN)
})
it('유니코드/이모지 원문이 그대로 저장된다', () => {
const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.UNICODE }))
expect(getHistoryService().getById(entry.id)?.originalText).toBe(USER_TEXT.UNICODE)
})
it('긴 원문(4000자)이 잘리지 않고 저장된다', () => {
const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.LONG, wordCount: 4000 }))
expect(getHistoryService().getById(entry.id)?.originalText.length).toBe(4000)
})
it('빈 원문도 오류 상태 엔트리로 저장할 수 있다', () => {
const entry = getHistoryService().create(
historyInput({ originalText: USER_TEXT.EMPTY, status: 'error', errorCode: 'STTNoAudioData', wordCount: 0 }),
)
expect(entry.status).toBe('error')
expect(entry.errorCode).toBe('STTNoAudioData')
expect(getHistoryService().getById(entry.id)?.errorCode).toBe('STTNoAudioData')
})
it('취소된 세션은 cancelled 상태로 남는다', () => {
const entry = getHistoryService().create(historyInput({ status: 'cancelled', originalText: '' }))
expect(getHistoryService().getById(entry.id)?.status).toBe('cancelled')
})
it('같은 원문을 두 번 저장하면 서로 다른 id의 두 건이 된다', () => {
const a = getHistoryService().create(historyInput({ originalText: USER_TEXT.DUP }))
const b = getHistoryService().create(historyInput({ originalText: USER_TEXT.DUP }))
expect(a.id).not.toBe(b.id)
expect(getHistoryService().list({ page: 0, pageSize: 10 }).total).toBe(2)
})
it('존재하지 않는 id 조회는 null이다', () => {
expect(getHistoryService().getById('missing-id')).toBeNull()
})
it('한 건 삭제 후 목록에서 사라진다', () => {
const a = getHistoryService().create(historyInput())
const b = getHistoryService().create(historyInput({ originalText: USER_TEXT.EN }))
expect(getHistoryService().delete(a.id)).toBe(true)
expect(getHistoryService().getById(a.id)).toBeNull()
expect(getHistoryService().getById(b.id)).not.toBeNull()
})
it('없는 id 삭제는 false를 반환한다', () => {
expect(getHistoryService().delete('no-such')).toBe(false)
})
it('전체 삭제는 목록과 통계 조회가 가능한 빈 상태를 만든다', () => {
getHistoryService().create(historyInput())
getHistoryService().create(historyInput())
getHistoryService().deleteAll()
expect(getHistoryService().list({ page: 0, pageSize: 20 }).total).toBe(0)
const stats = getHistoryService().getStats()
expect(stats.todaySessionCount).toBe(0)
})
it('원문 검색은 부분 일치한다', () => {
getHistoryService().create(historyInput({ originalText: USER_TEXT.KO }))
getHistoryService().create(historyInput({ originalText: USER_TEXT.EN }))
const found = getHistoryService().search({ query: '회의', page: 0, pageSize: 10 })
expect(found.total).toBe(1)
expect(found.entries[0].originalText).toBe(USER_TEXT.KO)
})
it('검색어가 없으면 매칭 0건이다', () => {
getHistoryService().create(historyInput())
const found = getHistoryService().search({ query: 'zzzz-no-hit', page: 0, pageSize: 10 })
expect(found.total).toBe(0)
expect(found.entries).toEqual([])
})
it('페이지네이션 두 번째 페이지는 첫 페이지와 겹치지 않는다', () => {
for (let i = 0; i < 5; i++) {
getHistoryService().create(historyInput({ originalText: `item-${i}` }))
}
const p0 = getHistoryService().list({ page: 0, pageSize: 2 })
const p1 = getHistoryService().list({ page: 1, pageSize: 2 })
expect(p0.entries).toHaveLength(2)
expect(p1.entries).toHaveLength(2)
const ids = new Set([...p0.entries, ...p1.entries].map((e) => e.id))
expect(ids.size).toBe(4)
expect(p0.totalPages).toBe(3)
})
it('완료 세션은 오늘 통계에 반영된다', () => {
getHistoryService().create(historyInput({ duration: 2, wordCount: 4 }))
getHistoryService().create(historyInput({ duration: 3, wordCount: 6 }))
const stats = getHistoryService().getStats()
expect(stats.todaySessionCount).toBe(2)
expect(stats.todayWordCount).toBe(10)
expect(stats.totalSessionCount).toBe(2)
expect(stats.totalWordCount).toBe(10)
})
it('error 상태 세션은 오늘 완료 통계에 포함되지 않는다', () => {
getHistoryService().create(historyInput({ status: 'error', duration: 10, wordCount: 99 }))
const stats = getHistoryService().getStats()
expect(stats.todaySessionCount).toBe(0)
})
it('30일 이내 항목은 보존 정리에서 삭제되지 않는다', () => {
const entry = getHistoryService().create(historyInput())
expect(getHistoryService().runRetentionCleanup()).toBe(0)
expect(getHistoryService().getById(entry.id)).not.toBeNull()
})
it('IPC history:getAll 은 서비스 list 결과를 감싼다', async () => {
registerHistoryHandlers()
getHistoryService().create(historyInput({ originalText: USER_TEXT.KO }))
const res = await invokeIpc(IPC_CHANNELS.HISTORY.GET_ALL, { page: 0, pageSize: 10 })
expect(res.success).toBe(true)
if (res.success) {
expect(res.data.total).toBe(1)
expect(res.data.entries[0].originalText).toBe(USER_TEXT.KO)
}
})
it('IPC history:getById 없는 id 는 성공+null 이 아니라 조회 결과를 그대로 전달한다', async () => {
registerHistoryHandlers()
const res = await invokeIpc(IPC_CHANNELS.HISTORY.GET_BY_ID, { id: 'missing' })
expect(res.success).toBe(true)
if (res.success) expect(res.data).toBeNull()
})
it('IPC history:delete 없는 id 는 HistoryNotFound 로 실패한다', async () => {
registerHistoryHandlers()
const res = await invokeIpc(IPC_CHANNELS.HISTORY.DELETE, { id: 'missing' })
expect(res.success).toBe(false)
if (!res.success) expect(res.error.code).toBe(ErrorCode.HistoryNotFound)
})
it('IPC history:delete 있는 id 는 성공하고 목록에서 사라진다', async () => {
registerHistoryHandlers()
const entry = getHistoryService().create(historyInput())
const res = await invokeIpc(IPC_CHANNELS.HISTORY.DELETE, { id: entry.id })
expect(res.success).toBe(true)
expect(getHistoryService().getById(entry.id)).toBeNull()
})
it('IPC history:search 는 사용자 검색어로 필터한다', async () => {
registerHistoryHandlers()
getHistoryService().create(historyInput({ originalText: USER_TEXT.KO }))
const res = await invokeIpc(IPC_CHANNELS.HISTORY.SEARCH, { query: '오후', page: 0, pageSize: 10 })
expect(res.success).toBe(true)
if (res.success) expect(res.data.total).toBe(1)
})
it('IPC history:deleteAll 후 목록이 비고 stats:getSummary 가 동작한다', async () => {
registerHistoryHandlers()
getHistoryService().create(historyInput())
const del = await invokeIpc(IPC_CHANNELS.HISTORY.DELETE_ALL)
expect(del.success).toBe(true)
const stats = await invokeIpc(IPC_CHANNELS.STATS.GET_SUMMARY)
expect(stats.success).toBe(true)
if (stats.success) expect(stats.data.todaySessionCount).toBe(0)
})
it('짧은 원문(<10자)은 자동 제목을 만들지 않고 title 이 null 이다', async () => {
const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.SHORT }))
const title = await getHistoryService().generateTitle(entry.id)
expect(title).toBeNull()
expect(getHistoryService().getById(entry.id)?.title ?? null).toBeNull()
})
it('없는 엔트리 generateTitle 은 null 이다', async () => {
expect(await getHistoryService().generateTitle('missing')).toBeNull()
})
it('LLM 제목 생성 실패 시 create 는 성공하고 title 은 비어 있다', async () => {
const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.KO }))
expect(entry.id).toBeTruthy()
await new Promise((r) => setTimeout(r, 20))
const stored = getHistoryService().getById(entry.id)
expect(stored).not.toBeNull()
expect(stored?.title ?? null).toBeNull()
})
})