import { EventEmitter } from 'events' import { readFileSync } from 'fs' import { resolve } from 'path' import { promisify } from 'util' import { beforeEach, describe, expect, it, vi } from 'vitest' const childProcess = vi.hoisted(() => ({ exec: vi.fn(), execFile: vi.fn(), execFileAsync: vi.fn(), spawn: vi.fn(), })) vi.mock('child_process', () => childProcess) vi.mock('../../../src/main/services/LoggerService', () => ({ getLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }), })) vi.mock('../../../src/main/services/ConfigService', () => ({ configGet: vi.fn(), configSet: vi.fn(), })) vi.mock('../../../src/main/services/stt/STTManager', () => ({ getSTTManager: vi.fn(), })) vi.mock('../../../src/main/services/HistoryService', () => ({ getHistoryService: vi.fn(), })) vi.mock('../../../src/main/services/RuntimeProvisioner', () => ({ getRuntimeProvisioner: vi.fn(), })) vi.mock('../../../src/main/services/PremiumLLMService', () => ({ getPremiumLLMService: vi.fn(), })) function createProcess(args: unknown[]): EventEmitter & { stdout: EventEmitter stderr: EventEmitter kill: ReturnType } { const process = Object.assign(new EventEmitter(), { stdout: new EventEmitter(), stderr: new EventEmitter(), kill: vi.fn(), }) queueMicrotask(() => { if (args.includes('null')) { process.stderr.emit('data', Buffer.from('Duration: 00:00:01.00')) } if (args.includes('pipe:1')) { process.stdout.emit('data', Buffer.from('pcm')) } process.emit('close', 0) }) return process } function mockSuccessfulExec(): void { childProcess.exec.mockImplementation((...args: unknown[]) => { const callback = args.find((arg): arg is (error: Error | null, stdout: string) => void => typeof arg === 'function') callback?.(null, '[{"InstanceId":"device-1","FriendlyName":"Mic"}]') }) } function expectWindowsHideOnAllCalls(mock: { mock: { calls: unknown[][] } }): void { for (const call of mock.mock.calls) { const options = call.find( (arg): arg is { windowsHide?: boolean } => typeof arg === 'object' && arg !== null && !Array.isArray(arg), ) expect(options?.windowsHide).toBe(true) } } beforeEach(() => { vi.resetModules() vi.clearAllMocks() mockSuccessfulExec() childProcess.spawn.mockImplementation((_command: unknown, args: unknown[]) => createProcess(args)) Reflect.set( childProcess.execFile, promisify.custom, childProcess.execFileAsync, ) childProcess.execFileAsync.mockResolvedValue({ stdout: 'TestApp\nTest window', stderr: '' }) }) describe('Windows child process visibility', () => { it('hides the PowerShell SAPI process used for TTS playback', async () => { const serviceModule = await import('../../../src/main/services/TTSPlaybackService') serviceModule.resetTTSPlaybackServiceForTests() const service = serviceModule.getTTSPlaybackService() as unknown as { _speakOneWindows(text: string): Promise } await service._speakOneWindows('테스트') expect(childProcess.spawn).toHaveBeenCalledTimes(1) expect(childProcess.spawn.mock.calls[0]?.[0]).toBe('powershell') expect(childProcess.spawn.mock.calls[0]?.[2]).toMatchObject({ stdio: 'pipe', windowsHide: true }) }) it('hides cmd and PowerShell commands invoked by voice actions', async () => { const serviceModule = await import('../../../src/main/services/VoiceActionService') serviceModule.resetVoiceActionServiceForTests() const service = serviceModule.getVoiceActionService() as unknown as { _openApp(appName: string): Promise _simulateKeyboard(combo: string): Promise _runCommand(command: string): Promise } await service._openApp('notepad') await service._simulateKeyboard('volumeup') await service._simulateKeyboard('volumedown') await service._simulateKeyboard('volumemute') await service._simulateKeyboard('ctrl+c') await service._runCommand('echo test') expect(childProcess.exec).toHaveBeenCalledTimes(6) expect(childProcess.exec.mock.calls[0]?.[1]).toMatchObject({ shell: 'cmd.exe', windowsHide: true }) expect(childProcess.exec.mock.calls[5]?.[1]).toMatchObject({ timeout: 10000, windowsHide: true }) expectWindowsHideOnAllCalls(childProcess.exec) }) // 장치 목록·활성 창 조회는 win32 분기에서만 PowerShell 을 부른다. it.skipIf(process.platform !== 'win32')('hides Windows device discovery and active-window PowerShell calls', async () => { const audioModule = await import('../../../src/main/services/AudioCaptureService') const audioService = audioModule.getAudioCaptureService() as unknown as { _getDevicesWindows(): Promise } await audioService._getDevicesWindows() const contextModule = await import('../../../src/main/services/ScreenContextService') const contextService = contextModule.getScreenContextService() await contextService.captureContext(false) expect(childProcess.exec.mock.calls[0]?.[1]).toMatchObject({ encoding: 'utf8', timeout: 3000, windowsHide: true, }) expect(childProcess.execFileAsync.mock.calls[0]?.[2]).toMatchObject({ timeout: 3000, windowsHide: true, }) }) it('declares hidden ffmpeg processes for conversion, duration, and chunk extraction', () => { const source = readFileSync( resolve(process.cwd(), 'src/main/services/FileTranscriptionService.ts'), 'utf8', ) const ffmpegSpawns = source.match( /spawn\(ffmpegPath, args, \{ stdio: \['pipe', 'pipe', 'pipe'\], windowsHide: true \}\)/g, ) expect(ffmpegSpawns).toHaveLength(3) }) })