// RED 유스케이스 공통 하네스: 실제 서비스 + in-memory DB + IPC 캡처 // 네트워크/모델 I/O 만 더블로 교체한다. import { afterEach, beforeEach, vi } from 'vitest' import { ipcMain } from 'electron' import { createTestDb } from '../helpers/createTestDb' import { bindTestDatabase, unbindTestDatabase } from '../../src/main/db' import { initInMemoryConfig, resetInMemoryConfig, } from '../../src/main/services/ConfigService' import { resetHistoryServiceForTests } from '../../src/main/services/HistoryService' import { resetDictionaryServiceForTests } from '../../src/main/services/DictionaryService' import { resetCustomInstructionServiceForTests } from '../../src/main/services/CustomInstructionService' import { resetChainServiceForTests } from '../../src/main/services/ChainService' import { resetVoiceCommandServiceForTests } from '../../src/main/services/VoiceCommandService' import { resetLicenseServiceForTests } from '../../src/main/services/LicenseService' import { resetVoiceModeServiceForTests } from '../../src/main/services/VoiceModeService' import { resetMemoServiceForTests } from '../../src/main/services/MemoService' import { resetMeetingModeServiceForTests } from '../../src/main/services/MeetingModeService' import { resetMeetingDocTemplateServiceForTests } from '../../src/main/services/MeetingDocTemplateService' import { resetDictationTemplateServiceForTests } from '../../src/main/services/DictationTemplateService' import { resetVoiceConversationServiceForTests } from '../../src/main/services/VoiceConversationService' import { resetRAGServiceForTests } from '../../src/main/services/RAGService' import { resetVoiceActionServiceForTests } from '../../src/main/services/VoiceActionService' import { resetCaptionServiceForTests } from '../../src/main/services/CaptionService' import { resetFileTranscriptionServiceForTests } from '../../src/main/services/FileTranscriptionService' import { resetCloudSTTServiceForTests } from '../../src/main/services/CloudSTTService' import { resetPremiumLLMServiceForTests } from '../../src/main/services/PremiumLLMService' import { resetOnlineLLMServiceForTests } from '../../src/main/services/OnlineLLMService' import { resetCloudSyncServiceForTests } from '../../src/main/services/CloudSyncService' import { resetTTSPlaybackServiceForTests } from '../../src/main/services/TTSPlaybackService' import { resetMeetingSummaryServiceForTests } from '../../src/main/services/MeetingSummaryService' import { resetLocalLLMServiceForTests } from '../../src/main/services/LocalLLMService' import { resetLocalSTTServiceForTests } from '../../src/main/services/LocalSTTService' import type { IPCResult } from '@d3ro/core/errors' import type { NewHistory } from '../../src/main/db/schema' const ipcHandlers = new Map unknown>() export function resetIpcHandlers(): void { ipcHandlers.clear() vi.mocked(ipcMain.handle).mockImplementation((channel: string, handler: (...args: unknown[]) => unknown) => { ipcHandlers.set(channel, handler) }) } export async function invokeIpc( channel: string, ...args: unknown[] ): Promise> { const handler = ipcHandlers.get(channel) if (!handler) { throw new Error(`IPC handler not registered: ${channel}`) } return (await handler({}, ...args)) as IPCResult } export function hasIpc(channel: string): boolean { return ipcHandlers.has(channel) } export function historyInput( overrides: Partial> = {}, ): Omit { return { originalText: '오늘 회의는 오후 세 시에 시작합니다', duration: 3.5, wordCount: 8, mode: 'dictation', status: 'completed', appVersion: '1.0.0', ...overrides, } } export function resetAllSingletons(): void { resetHistoryServiceForTests() resetDictionaryServiceForTests() resetCustomInstructionServiceForTests() resetChainServiceForTests() resetVoiceCommandServiceForTests() resetLicenseServiceForTests() resetVoiceModeServiceForTests() resetMemoServiceForTests() resetMeetingModeServiceForTests() resetMeetingDocTemplateServiceForTests() resetDictationTemplateServiceForTests() resetVoiceConversationServiceForTests() resetRAGServiceForTests() resetVoiceActionServiceForTests() resetCaptionServiceForTests() resetFileTranscriptionServiceForTests() resetCloudSTTServiceForTests() resetPremiumLLMServiceForTests() resetOnlineLLMServiceForTests() resetCloudSyncServiceForTests() resetTTSPlaybackServiceForTests() resetMeetingSummaryServiceForTests() resetLocalLLMServiceForTests() resetLocalSTTServiceForTests() } export function useRedHarness(): void { let testdb: ReturnType | null = null beforeEach(() => { testdb = createTestDb() bindTestDatabase(testdb.db) initInMemoryConfig() resetAllSingletons() resetIpcHandlers() }) afterEach(() => { resetAllSingletons() unbindTestDatabase() testdb?.close() testdb = null resetInMemoryConfig() vi.unstubAllGlobals() }) } export type InvokeFnResult = { data: unknown; error: { message: string } | null } export function makeCloudSyncFake(opts?: { authenticated?: boolean invoke?: (name: string, body: Record) => Promise }): { isEnabled: () => boolean isAuthenticated: () => boolean getState: () => { authenticated: boolean userEmail: string | null lastSyncAt: number | null syncing: boolean } getUser: () => { id: string; email: string } | null invokeFunction: (name: string, body: Record) => Promise pushOne: () => Promise pushAll: () => Promise<{ pushed: number; errors: string[] }> pullAll: () => Promise<{ pushed: number; errors: string[] }> startSignIn: (provider: string) => Promise handleAuthCallback: (code: string) => Promise signOut: () => Promise signInAnonymously: () => Promise on: () => void off: () => void } { const authenticated = opts?.authenticated ?? false return { isEnabled: () => true, isAuthenticated: () => authenticated, getState: () => ({ authenticated, userEmail: authenticated ? 'fx.user@example.test' : null, lastSyncAt: null, syncing: false, }), getUser: () => authenticated ? { id: 'fx-user-id', email: 'fx.user@example.test' } : null, invokeFunction: opts?.invoke ?? (async () => ({ data: null, error: { message: 'fx.cloud.unconfigured' } })), pushOne: async () => undefined, pushAll: async () => ({ pushed: 0, errors: [] }), pullAll: async () => ({ pushed: 0, errors: [] }), startSignIn: async () => { throw new Error('fx.oauth.not-started') }, handleAuthCallback: async () => { throw new Error('fx.oauth.callback-rejected') }, signOut: async () => undefined, signInAnonymously: async () => { throw new Error('fx.anon.not-supported') }, on: () => undefined, off: () => undefined, } }