feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
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
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
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
208
apps/desktop/tests/red/history.usecase.test.ts
Normal file
208
apps/desktop/tests/red/history.usecase.test.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
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()
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue