import { describe, it, expect, vi } from 'vitest' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { ErrorCode } from '@d3ro/core/errors' import { configGet } from '../../src/main/services/ConfigService' import { registerHotkeyHandlers } from '../../src/main/ipc/hotkey-handlers' import { registerCaptionHandlers } from '../../src/main/ipc/caption-handlers' import { registerRAGHandlers } from '../../src/main/ipc/rag-handlers' import { registerVoiceConversationHandlers } from '../../src/main/ipc/voice-conversation-handlers' import { registerVoiceActionHandlers } from '../../src/main/ipc/voice-action-handlers' import { registerFileTranscriptionHandlers } from '../../src/main/ipc/file-transcription-handlers' import { registerMeetingModeHandlers } from '../../src/main/ipc/meeting-mode-handlers' import { registerTemplateHandlers } from '../../src/main/ipc/template-handlers' import { registerMeetingDocTemplateHandlers } from '../../src/main/ipc/meeting-doc-template-handlers' import { invokeIpc, useRedHarness } from './harness' vi.mock('../../src/main/services/HotkeyService', () => ({ getHotkeyService: () => ({ loadFromConfig: vi.fn(), start: vi.fn(), stop: vi.fn(), on: vi.fn(), off: vi.fn(), }), })) useRedHarness() const SAMPLE_BINDING = { keyCode: 65, ctrl: true, alt: false, shift: false, meta: false, displayLabel: 'Ctrl+A', } describe('유스케이스: 핫키 / 캡션 / RAG / 대화 / 액션 / 파일전사 / 회의 IPC', () => { it('핫키 받아쓰기 단축키를 읽고 쓴다', async () => { registerHotkeyHandlers() const set = await invokeIpc(IPC_CHANNELS.HOTKEY.SET_DICTATION_SHORTCUT, { binding: SAMPLE_BINDING }) expect(set.success).toBe(true) const get = await invokeIpc(IPC_CHANNELS.HOTKEY.GET_DICTATION_SHORTCUT) expect(get.success).toBe(true) if (get.success) expect(get.data.displayLabel).toBe('Ctrl+A') }) it('핫키 핸즈프리 단축키를 읽고 쓴다', async () => { registerHotkeyHandlers() await invokeIpc(IPC_CHANNELS.HOTKEY.SET_HANDS_FREE_SHORTCUT, { binding: SAMPLE_BINDING }) const get = await invokeIpc(IPC_CHANNELS.HOTKEY.GET_HANDS_FREE_SHORTCUT) expect(get.success).toBe(true) }) it('핫키 명령 단축키를 읽고 쓴다', async () => { registerHotkeyHandlers() await invokeIpc(IPC_CHANNELS.HOTKEY.SET_COMMAND_SHORTCUT, { binding: SAMPLE_BINDING }) const get = await invokeIpc(IPC_CHANNELS.HOTKEY.GET_COMMAND_SHORTCUT) expect(get.success).toBe(true) }) it('핫키 자막 단축키를 읽고 쓴다', async () => { registerHotkeyHandlers() await invokeIpc(IPC_CHANNELS.HOTKEY.SET_CAPTION_SHORTCUT, { binding: SAMPLE_BINDING }) const get = await invokeIpc(IPC_CHANNELS.HOTKEY.GET_CAPTION_SHORTCUT) expect(get.success).toBe(true) }) it('핫키 활성 토글을 끈다', async () => { registerHotkeyHandlers() const set = await invokeIpc(IPC_CHANNELS.HOTKEY.SET_ENABLED, { enabled: false }) expect(set.success).toBe(true) const get = await invokeIpc(IPC_CHANNELS.HOTKEY.IS_ENABLED) expect(get.success).toBe(true) if (get.success) expect(get.data).toBe(false) expect(configGet('hotkeyEnabled')).toBe(false) }) it('핫키 활성 토글을 켠다', async () => { registerHotkeyHandlers() await invokeIpc(IPC_CHANNELS.HOTKEY.SET_ENABLED, { enabled: true }) const get = await invokeIpc(IPC_CHANNELS.HOTKEY.IS_ENABLED) if (get.success) expect(get.data).toBe(true) }) it('캡션 초기 상태를 읽는다', async () => { registerCaptionHandlers() const res = await invokeIpc(IPC_CHANNELS.CAPTION.GET_STATE) expect(res.success).toBe(true) if (res.success) expect(res.data).toBe('inactive') }) it('캡션 설정을 바꾼다', async () => { registerCaptionHandlers() const res = await invokeIpc(IPC_CHANNELS.CAPTION.SET_CONFIG, { fontSize: 20, opacity: 0.5 }) expect(res.success).toBe(true) }) it('캡션 시작 실패는 success:false 로 나온다', async () => { registerCaptionHandlers() const res = await invokeIpc(IPC_CHANNELS.CAPTION.START) if (res.success) { const stop = await invokeIpc(IPC_CHANNELS.CAPTION.STOP) expect(stop.success).toBe(true) } else { expect(res.error.code).toBeTruthy() } }) it('RAG 문서 목록 IPC', async () => { registerRAGHandlers() const res = await invokeIpc(IPC_CHANNELS.RAG.GET_DOCUMENTS) expect(res.success).toBe(true) if (res.success) expect(Array.isArray(res.data)).toBe(true) }) it('RAG 질의 실패는 에러다', async () => { registerRAGHandlers() const res = await invokeIpc(IPC_CHANNELS.RAG.QUERY, { query: 'hello', topK: 3 }) expect(res.success).toBe(false) }) it('대화 상태를 읽는다', async () => { registerVoiceConversationHandlers() const res = await invokeIpc(IPC_CHANNELS.VOICE_CONVERSATION.GET_STATE) expect(res.success).toBe(true) }) it('대화 히스토리를 읽는다', async () => { registerVoiceConversationHandlers() const res = await invokeIpc(IPC_CHANNELS.VOICE_CONVERSATION.GET_HISTORY) expect(res.success).toBe(true) if (res.success) expect(Array.isArray(res.data)).toBe(true) }) it('세션 없이 대화 메시지는 실패한다', async () => { registerVoiceConversationHandlers() const res = await invokeIpc(IPC_CHANNELS.VOICE_CONVERSATION.SEND_MESSAGE, { text: 'hi' }) expect(res.success).toBe(false) }) it('보이스 액션 프리셋을 읽는다', async () => { registerVoiceActionHandlers() const res = await invokeIpc(IPC_CHANNELS.VOICE_ACTION.GET_PRESETS) expect(res.success).toBe(true) if (res.success) expect((res.data as unknown[]).length).toBeGreaterThan(0) }) it('보이스 액션 활성 토글', async () => { registerVoiceActionHandlers() await invokeIpc(IPC_CHANNELS.VOICE_ACTION.SET_ENABLED, { enabled: true }) const res = await invokeIpc(IPC_CHANNELS.VOICE_ACTION.IS_ENABLED) expect(res.success).toBe(true) if (res.success) expect(res.data).toBe(true) }) it('파일 전사 상태를 읽는다', async () => { registerFileTranscriptionHandlers() const res = await invokeIpc(IPC_CHANNELS.FILE_TRANSCRIPTION.GET_STATE) expect(res.success).toBe(true) if (res.success) expect(res.data.state).toBe('idle') }) it('파일 전사 잘못된 경로는 실패한다', async () => { registerFileTranscriptionHandlers() const res = await invokeIpc(IPC_CHANNELS.FILE_TRANSCRIPTION.START, { filePath: 'nope.txt' }) expect(res.success).toBe(false) }) it('회의 상태를 읽는다', async () => { registerMeetingModeHandlers() const res = await invokeIpc(IPC_CHANNELS.MEETING_MODE.GET_STATE) expect(res.success).toBe(true) }) it('회의 세션 목록을 읽는다', async () => { registerMeetingModeHandlers() const res = await invokeIpc(IPC_CHANNELS.MEETING_MODE.GET_SESSIONS, { page: 1, pageSize: 10 }) expect(res.success).toBe(true) }) it('없는 회의 세션 조회는 실패한다', async () => { registerMeetingModeHandlers() const res = await invokeIpc(IPC_CHANNELS.MEETING_MODE.GET_SESSION, { sessionId: 'missing' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.MeetingSessionNotFound) }) it('딕테이션 템플릿 목록 IPC', async () => { registerTemplateHandlers() const res = await invokeIpc(IPC_CHANNELS.DICTATION_TEMPLATE.GET_ALL) expect(res.success).toBe(true) if (res.success) expect((res.data as unknown[]).length).toBeGreaterThan(0) }) it('없는 딕테이션 템플릿 세션 시작은 실패한다', async () => { registerTemplateHandlers() const res = await invokeIpc(IPC_CHANNELS.DICTATION_TEMPLATE.START_SESSION, { templateId: 'missing' }) expect(res.success).toBe(false) }) it('회의 문서 템플릿 목록 IPC', async () => { registerMeetingDocTemplateHandlers() const res = await invokeIpc(IPC_CHANNELS.MEETING_DOC_TEMPLATE.GET_ALL) expect(res.success).toBe(true) if (res.success) expect((res.data as unknown[]).length).toBeGreaterThan(0) }) it('빌트인 회의 템플릿 삭제는 실패한다', async () => { registerMeetingDocTemplateHandlers() const res = await invokeIpc(IPC_CHANNELS.MEETING_DOC_TEMPLATE.DELETE, { id: 'builtin-minutes' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.MeetingDocTemplateBuiltinDelete) }) })