import { EventEmitter } from 'events' import { beforeEach, describe, expect, it, vi } from 'vitest' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { ErrorCode } from '@d3ro/core/errors' import type { IPCResult } from '@d3ro/core/errors' import type { CaptionSegment, MeetingModeStateInfo } from '@d3ro/core/types' // 회의 전사 세그먼트 편집이 main 에 핸들러가 없는 채널을 호출하던 회귀 // (preload editSegment → 'meetingMode:editSegment' → No handler registered). const invokeHandlers = vi.hoisted( () => new Map unknown>(), ) type FakeMeetingService = EventEmitter & { stateInfo: MeetingModeStateInfo getStateInfo: () => MeetingModeStateInfo updateTranscript: ReturnType } // vi.mock 팩토리는 모듈을 처음 import 할 때(beforeEach 안) 불리므로 이 상수는 이미 초기화되어 있다. const service = Object.assign(new EventEmitter(), { stateInfo: { state: 'recording', sessionId: 's1', elapsedMs: 0, memoCount: 0, segmentCount: 0 }, getStateInfo(this: FakeMeetingService): MeetingModeStateInfo { return this.stateInfo }, updateTranscript: vi.fn(), }) as FakeMeetingService vi.mock('electron', () => ({ ipcMain: { handle: vi.fn((channel: string, handler: (event: unknown, ...args: unknown[]) => unknown) => { invokeHandlers.set(channel, handler) }), on: vi.fn(), }, })) vi.mock('../../../src/main/services/MeetingModeService', () => ({ getMeetingModeService: () => service, })) async function invoke(channel: string, ...args: unknown[]): Promise> { const handler = invokeHandlers.get(channel) if (!handler) throw new Error(`No handler registered for '${channel}'`) return (await handler({}, ...args)) as IPCResult } function segment(id: string, text: string): CaptionSegment { return { id, text, timestamp: 0, isFinal: true } } beforeEach(async () => { vi.resetModules() invokeHandlers.clear() service.removeAllListeners() service.updateTranscript.mockReset() service.stateInfo = { state: 'recording', sessionId: 's1', elapsedMs: 0, memoCount: 0, segmentCount: 0 } const mod = await import('../../../src/main/ipc/meeting-mode-handlers') mod.registerMeetingModeHandlers() }) describe('MEETING_MODE.EDIT_SEGMENT', () => { it('핸들러가 등록되어 있다', () => { expect(invokeHandlers.has(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT)).toBe(true) }) it('녹음 중 수정이 세션 완료 시 editedTranscript 로 저장된다', async () => { service.emit('segment', segment('a', '안녕하세요')) service.emit('segment', segment('b', '오늘 안건')) const res = await invoke(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, { sessionId: 's1', segmentId: 'b', text: '오늘 안건은 예산', }) expect(res.success).toBe(true) service.emit('session-completed', { id: 's1', rawTranscript: '[00:01] 안녕하세요\n[00:02] 오늘 안건', }) expect(service.updateTranscript).toHaveBeenCalledWith( 's1', '[00:01] 안녕하세요\n[00:02] 오늘 안건은 예산', ) }) it('진행 중이 아닌 세션이면 MeetingNotRecording 으로 거부한다', async () => { service.stateInfo = { ...service.stateInfo, state: 'idle', sessionId: null } const res = await invoke(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, { sessionId: 's1', segmentId: 'a', text: 'x', }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.MeetingNotRecording) }) it('모르는 세그먼트는 MeetingTranscriptUpdateFailed 로 거부한다', async () => { const res = await invoke(IPC_CHANNELS.MEETING_MODE.EDIT_SEGMENT, { sessionId: 's1', segmentId: 'nope', text: 'x', }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.MeetingTranscriptUpdateFailed) }) it('다시 등록해도 세션 완료 리스너가 중복되지 않는다', async () => { const mod = await import('../../../src/main/ipc/meeting-mode-handlers') mod.registerMeetingModeHandlers() expect(service.listenerCount('session-completed')).toBe(1) expect(service.listenerCount('segment')).toBe(1) }) })