import { describe, it, expect } from 'vitest' import { D3ROError, ErrorCode } from '@d3ro/core/errors' import { getMeetingModeService } from '../../src/main/services/MeetingModeService' import { getMeetingSummaryService } from '../../src/main/services/MeetingSummaryService' import { getHistoryService } from '../../src/main/services/HistoryService' import { getDatabase } from '../../src/main/db' import { meetingSessions } from '../../src/main/db/schema' import { historyInput, useRedHarness } from './harness' import { USER_TEXT } from './fixtures' useRedHarness() function insertSession(id: string, overrides: Record = {}): void { const now = Date.now() getDatabase() .insert(meetingSessions) .values({ id, title: '세션', status: 'completed', startedAt: now, endedAt: now + 1000, durationMs: 1000, rawTranscript: USER_TEXT.KO, createdAt: now, updatedAt: now, ...overrides, }) .run() } describe('유스케이스: 회의 세션 CRUD / 메모 / 전사 수정 / 요약 실패', () => { it('초기 회의 상태는 idle 이다', () => { expect(getMeetingModeService().getState()).toBe('idle') expect(getMeetingModeService().getStateInfo().sessionId).toBeNull() }) it('녹음 중이 아닐 때 메모 추가는 MeetingNotRecording 이다', async () => { await expect(getMeetingModeService().addMemo('메모')).rejects.toMatchObject({ code: ErrorCode.MeetingNotRecording, }) }) it('녹음 중이 아닐 때 중지는 MeetingNotRecording 이다', async () => { await expect(getMeetingModeService().stopRecording()).rejects.toMatchObject({ code: ErrorCode.MeetingNotRecording, }) }) it('세션 목록은 처음에 비어 있다', () => { const page = getMeetingModeService().getSessions(1, 10) expect(page.total).toBe(0) expect(page.sessions).toEqual([]) }) it('저장된 세션을 목록에서 본다', () => { insertSession('sess-1', { title: '주간회의' }) const page = getMeetingModeService().getSessions(1, 10) expect(page.total).toBe(1) expect(page.sessions[0].title).toBe('주간회의') }) it('세션 상세를 연다', () => { insertSession('sess-2', { title: '상세' }) const detail = getMeetingModeService().getSession('sess-2') expect(detail.id).toBe('sess-2') expect(detail.title).toBe('상세') }) it('없는 세션 조회는 MeetingSessionNotFound 다', () => { expect(() => getMeetingModeService().getSession('missing')).toThrow(D3ROError) try { getMeetingModeService().getSession('missing') } catch (err) { expect((err as D3ROError).code).toBe(ErrorCode.MeetingSessionNotFound) } }) it('세션 제목을 바꾼다', () => { insertSession('sess-3', { title: 'old' }) getMeetingModeService().updateTitle('sess-3', 'new-title') expect(getMeetingModeService().getSession('sess-3').title).toBe('new-title') }) it('없는 세션 제목 변경은 MeetingSessionNotFound 다', () => { expect(() => getMeetingModeService().updateTitle('missing', 'x')).toThrow(D3ROError) }) it('세션을 삭제하면 목록에서 사라진다', () => { insertSession('sess-4') getMeetingModeService().deleteSession('sess-4') expect(getMeetingModeService().getSessions(1, 10).total).toBe(0) }) it('없는 세션 삭제는 MeetingSessionNotFound 다', () => { expect(() => getMeetingModeService().deleteSession('missing')).toThrow(D3ROError) }) it('전사를 수정하면 edited 텍스트가 남는다', () => { insertSession('sess-5') getMeetingModeService().updateTranscript('sess-5', USER_TEXT.EN) const detail = getMeetingModeService().getSession('sess-5') expect(detail.editedTranscript ?? detail.rawTranscript).toBeTruthy() }) it('없는 세션 전사 수정은 MeetingSessionNotFound 다', () => { expect(() => getMeetingModeService().updateTranscript('missing', 'x')).toThrow(D3ROError) }) it('빈 제목으로 바꾸면 빈 문자열이 저장되거나 거부된다', () => { insertSession('sess-6', { title: 'keep' }) try { getMeetingModeService().updateTitle('sess-6', ' ') const title = getMeetingModeService().getSession('sess-6').title expect(title === 'keep' || title?.trim() === '').toBe(true) } catch (err) { expect(err).toBeInstanceOf(D3ROError) } }) it('없는 세션의 문서 목록은 빈 배열이다', () => { expect(getMeetingModeService().getDocuments('missing')).toEqual([]) }) it('없는 문서 수정은 MeetingDocumentNotFound 다', () => { expect(() => getMeetingModeService().updateDocument('missing', 'x')).toThrow(D3ROError) }) it('없는 문서 삭제는 MeetingDocumentNotFound 다', () => { expect(() => getMeetingModeService().deleteDocument('missing')).toThrow(D3ROError) }) it('페이지네이션으로 세션을 나눈다', () => { insertSession('p-a', { title: 'A' }) insertSession('p-b', { title: 'B' }) insertSession('p-c', { title: 'C' }) const p0 = getMeetingModeService().getSessions(1, 2) expect(p0.sessions.length).toBe(2) expect(p0.totalPages).toBe(2) }) it('요약할 히스토리가 없으면 HistoryNotFound 다', async () => { await expect(getMeetingSummaryService().summarize('missing')).rejects.toMatchObject({ code: ErrorCode.HistoryNotFound, }) }) it('빈 전사는 MeetingSummaryNoTranscript 다', async () => { const entry = getHistoryService().create(historyInput({ originalText: '' })) await expect(getMeetingSummaryService().summarize(entry.id)).rejects.toMatchObject({ code: ErrorCode.MeetingSummaryNoTranscript, }) }) it('LLM 이 없으면 요약 실패가 표면화된다 (빈 성공 금지)', async () => { const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.KO })) await expect(getMeetingSummaryService().summarize(entry.id)).rejects.toBeTruthy() expect(getHistoryService().getById(entry.id)?.summaryText ?? null).toBeNull() }) it('저장된 요약이 없으면 getSummary 는 null 이다', () => { const entry = getHistoryService().create(historyInput()) expect(getMeetingSummaryService().getSummary(entry.id)).toBeNull() }) it('내보내기할 요약이 없으면 MeetingSummaryExportFailed 다', async () => { const entry = getHistoryService().create(historyInput()) await expect(getMeetingSummaryService().exportMarkdown(entry.id)).rejects.toMatchObject({ code: ErrorCode.MeetingSummaryExportFailed, }) }) })