import { describe, expect, it } from 'vitest' import type { MeetingDocument, MeetingSessionSummary } from '@d3ro/core/types' import { appendMeetingPage, filterMeetingsByTitle, hasMoreMeetings } from '../../src/renderer/pages/meeting/meeting-list' import { formatElapsed, shouldShowStatusBadge } from '../../src/renderer/components/meeting/meeting-format' import { clearNotepad, loadNotepad, notepadStorageKey, saveNotepad, type NotepadStorage, } from '../../src/renderer/components/meeting/meeting-notepad-storage' import { formatActionItemsForCopy, selectSummarySource, summaryCopyText, } from '../../src/renderer/components/meeting/meeting-summary-model' function session(id: string, title: string | null): MeetingSessionSummary { return { id, title, status: 'completed', startedAt: 0, endedAt: null, durationMs: null, memoCount: 0 } } function memoryStorage(): NotepadStorage & { data: Map } { const data = new Map() return { data, getItem: (k) => data.get(k) ?? null, setItem: (k, v) => void data.set(k, v), removeItem: (k) => void data.delete(k), } } describe('회의 목록 모델', () => { it('다음 페이지를 붙일 때 밀려 온 중복은 버린다', () => { const merged = appendMeetingPage([session('a', 'A'), session('b', 'B')], [session('b', 'B'), session('c', 'C')]) expect(merged.map((s) => s.id)).toEqual(['a', 'b', 'c']) expect(hasMoreMeetings(3, 3)).toBe(false) expect(hasMoreMeetings(2, 3)).toBe(true) }) it('제목 검색은 대소문자·공백을 무시하고, 제목 없는 회의는 대체 제목으로 찾는다', () => { const list = [session('a', '주간 Sync 회의'), session('b', null)] expect(filterMeetingsByTitle(list, 'sync 회의', '무제 회의').map((s) => s.id)).toEqual(['a']) expect(filterMeetingsByTitle(list, '무제', '무제 회의').map((s) => s.id)).toEqual(['b']) expect(filterMeetingsByTitle(list, ' ', '무제 회의')).toBe(list) }) it('완료는 배지를 달지 않고 예외 상태만 드러낸다', () => { expect(shouldShowStatusBadge('completed')).toBe(false) expect(shouldShowStatusBadge('error')).toBe(true) expect(formatElapsed(65_000)).toBe('01:05') expect(formatElapsed(3_725_000)).toBe('1:02:05') }) }) describe('내 메모 보관 — 닫아도 사라지지 않는다', () => { it('회의 id 별로 저장·복원하고, 비우거나 회의를 지우면 키를 지운다', () => { const storage = memoryStorage() expect(saveNotepad(storage, 's1', '- 예산 재검토')).toBe(true) expect(loadNotepad(storage, 's1')).toBe('- 예산 재검토') expect(loadNotepad(storage, 's2')).toBe('') saveNotepad(storage, 's1', ' ') expect(storage.data.has(notepadStorageKey('s1'))).toBe(false) saveNotepad(storage, 's1', 'x') clearNotepad(storage, 's1') expect(loadNotepad(storage, 's1')).toBe('') }) it('저장소가 막히면 실패를 알리고 던지지 않는다', () => { const broken: NotepadStorage = { getItem: () => { throw new Error('blocked') }, setItem: () => { throw new Error('quota') }, removeItem: () => { throw new Error('blocked') }, } expect(saveNotepad(broken, 's1', 'x')).toBe(false) expect(loadNotepad(broken, 's1')).toBe('') expect(() => clearNotepad(broken, 's1')).not.toThrow() expect(saveNotepad(null, 's1', 'x')).toBe(false) }) }) describe('요약 출처 — 1순위 문서 → 동기화된 회의록 → 없음', () => { const doc = (content: string): MeetingDocument => ({ id: 'd1', sessionId: 's1', templateType: 'minutes', title: '회의록', content, promptUsed: null, llmModel: null, llmLatencyMs: null, createdAt: 0, updatedAt: 0, }) const labels = { decisions: '결정 사항', actionItems: '할 일' } it('문서가 있으면 문서가 요약이다', () => { const source = selectSummarySource([doc('# 요약\n본문')], '다른 회의록', null) expect(source.kind).toBe('document') expect(summaryCopyText(source, labels)).toBe('# 요약\n본문') }) it('문서가 없으면 minutesMarkdown, 그다음 구조화된 minutes, 없으면 none', () => { expect(selectSummarySource([], ' md ', null).kind).toBe('markdown') const minutes = { summary: '요약', decisions: ['출시 연기'], actionItems: [{ task: '공지', assignee: '민수' }], timeline: [] } const source = selectSummarySource([], null, minutes) expect(source.kind).toBe('minutes') expect(summaryCopyText(source, labels)).toBe('요약\n\n## 결정 사항\n- 출시 연기\n\n## 할 일\n- [ ] 공지 (민수)') expect(selectSummarySource([], '', null).kind).toBe('none') expect(summaryCopyText({ kind: 'none' }, labels)).toBe('') }) it('할 일 복사는 체크리스트 형식', () => { expect(formatActionItemsForCopy([{ text: 'a', done: true }, { text: 'b', done: false }])).toBe('- [x] a\n- [ ] b') }) })