import { beforeEach, describe, expect, it, vi } from 'vitest' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' // ipcMain.handle(invoke 전용)과 ipcMain.on(send 전용)을 분리해 기록한다. // Electron에서 ipcRenderer.send 메시지는 handle 등록에 절대 도달하지 않으므로 // 이 둘을 섞어 쓰는 하네스로는 RecordingTip 클릭 취소 회귀를 잡을 수 없다. const invokeHandlers = vi.hoisted( () => new Map Promise>() ) const sendListeners = vi.hoisted( () => new Map void>>() ) const voice = vi.hoisted(() => ({ cancelSession: vi.fn(), on: vi.fn(), startSession: vi.fn(async () => undefined), stopSession: vi.fn(async () => undefined), getState: vi.fn(() => ({})), currentSession: null })) vi.mock('electron', () => ({ ipcMain: { handle: vi.fn((channel: string, handler: (event: unknown, ...args: unknown[]) => Promise) => { invokeHandlers.set(channel, handler) }), on: vi.fn((channel: string, listener: (event: unknown, ...args: unknown[]) => void) => { const list = sendListeners.get(channel) ?? [] list.push(listener) sendListeners.set(channel, list) }) } })) vi.mock('../../../src/main/services/VoiceModeService', () => ({ getVoiceModeService: () => voice })) vi.mock('../../../src/main/windows/WindowManager', () => ({ getMainWindow: () => null })) /** ipcRenderer.send 의미론 — ipcMain.on 리스너에만 전달된다 */ function simulateRendererSend(channel: string, ...args: unknown[]): number { const listeners = sendListeners.get(channel) ?? [] for (const listener of listeners) listener({}, ...args) return listeners.length } beforeEach(async () => { vi.resetModules() vi.clearAllMocks() invokeHandlers.clear() sendListeners.clear() const mod = await import('../../../src/main/ipc/voice-handlers') mod.registerVoiceHandlers() }) describe('VOICE.CANCEL_RECORDING — RecordingTip 팝업(send) 경로', () => { it('popupAPI.send로 보낸 취소가 세션을 취소한다', () => { const delivered = simulateRendererSend(IPC_CHANNELS.VOICE.CANCEL_RECORDING, {}) expect(delivered).toBe(1) expect(voice.cancelSession).toHaveBeenCalledTimes(1) }) it('RecordingTip 스크립트가 보내는 채널 문자열과 IPC 상수가 일치한다', () => { expect(IPC_CHANNELS.VOICE.CANCEL_RECORDING).toBe('voice:cancelRecording') }) }) describe('VOICE.CANCEL_RECORDING — 메인 윈도우(invoke) 경로', () => { it('invoke 경로도 여전히 세션을 취소하고 성공을 반환한다', async () => { const handler = invokeHandlers.get(IPC_CHANNELS.VOICE.CANCEL_RECORDING) if (!handler) throw new Error('cancel invoke handler was not registered') const result = await handler({}, {}) expect(voice.cancelSession).toHaveBeenCalledTimes(1) expect(result).toMatchObject({ success: true }) }) })