// tests/main/services/SoundEffectService-redteam-r1-15.test.ts // 회귀: Settings에서 효과음을 끄면(config.set → configSet) 재시작 없이 즉시 반영되어야 한다. import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import fs from 'fs' import os from 'os' import path from 'path' const SOUND_DIR = path.join(os.tmpdir(), 'd3ro-vitest-sounds-r1-15') const { execMock, execFileMock } = vi.hoisted(() => ({ execMock: vi.fn(), execFileMock: vi.fn() })) vi.mock('child_process', async (importOriginal) => { const actual = await importOriginal() return { ...actual, exec: execMock, execFile: execFileMock } }) vi.mock('../../../src/main/utils/paths', () => ({ getSoundPath: (filename: string) => path.join(SOUND_DIR, filename) })) import { configSet, initInMemoryConfig, resetInMemoryConfig } from '../../../src/main/services/ConfigService' import { SoundEffectService, getSoundEffectService, type SoundEffectDeps } from '../../../src/main/services/SoundEffectService' function playCount(): number { return execMock.mock.calls.length + execFileMock.mock.calls.length } beforeAll(() => { fs.mkdirSync(SOUND_DIR, { recursive: true }) for (const name of ['recording-start.wav', 'recording-stop.wav', 'error.wav']) { fs.writeFileSync(path.join(SOUND_DIR, name), Buffer.from('RIFF0000WAVE')) } }) describe('SoundEffectService — soundEnabled 변경 즉시 반영 (singleton + ConfigService)', () => { beforeEach(() => { execMock.mockReset() execFileMock.mockReset() initInMemoryConfig({ soundEnabled: true }) }) afterEach(() => { getSoundEffectService().dispose() resetInMemoryConfig() }) it('Settings가 configSet으로 soundEnabled=false를 쓰면 다음 play()는 재생하지 않는다', () => { const service = getSoundEffectService() service.initialize() expect(service.isEnabled()).toBe(true) // SettingsModal → config:set 핸들러 → configSet 경로 (setEnabled를 거치지 않음) configSet('soundEnabled', false) expect(service.isEnabled()).toBe(false) service.play('recording-start') service.play('error') expect(playCount()).toBe(0) // 다시 켜면 곧바로 재생된다 (동기화로 들어온 값도 같은 경로) configSet('soundEnabled', true) expect(service.isEnabled()).toBe(true) service.play('recording-stop') expect(playCount()).toBe(1) }) it('dispose 후에는 config 변경을 더 이상 구독하지 않는다', () => { const service = getSoundEffectService() service.initialize() service.dispose() configSet('soundEnabled', false) expect(service.isEnabled()).toBe(true) }) }) describe('SoundEffectService — 포트 주입 단위 테스트', () => { function createDeps(initial: boolean): { deps: SoundEffectDeps emit: (enabled: boolean) => void listenerCount: () => number played: string[] written: boolean[] } { const listeners = new Set<(enabled: boolean) => void>() const played: string[] = [] const written: boolean[] = [] let value = initial const deps: SoundEffectDeps = { settings: { read: () => value, write: (enabled) => { value = enabled written.push(enabled) listeners.forEach((l) => l(enabled)) }, subscribe: (listener) => { listeners.add(listener) return () => listeners.delete(listener) } }, output: { resolvePath: (filename) => `/sounds/${filename}`, load: (filePath) => (filePath.endsWith('error.wav') ? null : Buffer.from('wav')), play: (filePath) => played.push(filePath) } } return { deps, emit: (enabled) => { value = enabled listeners.forEach((l) => l(enabled)) }, listenerCount: () => listeners.size, played, written } } it('초기값을 설정 포트에서 읽고, 외부 변경을 따라간다', () => { const h = createDeps(false) const service = new SoundEffectService(h.deps) service.initialize() service.play('recording-start') expect(h.played).toEqual([]) h.emit(true) service.play('recording-start') service.play('chime') expect(h.played).toEqual(['/sounds/recording-start.wav', '/sounds/recording-stop.wav']) }) it('로드되지 않은 효과음은 건너뛴다', () => { const h = createDeps(true) const service = new SoundEffectService(h.deps) service.initialize() service.play('error') service.play('cancel') expect(h.played).toEqual([]) }) it('setEnabled는 설정 포트에 기록하고 상태를 갱신한다', () => { const h = createDeps(true) const service = new SoundEffectService(h.deps) service.initialize() service.setEnabled(false) expect(h.written).toEqual([false]) expect(service.isEnabled()).toBe(false) service.play('recording-start') expect(h.played).toEqual([]) }) it('initialize를 두 번 호출해도 구독은 하나만 유지되고 dispose가 해제한다', () => { const h = createDeps(true) const service = new SoundEffectService(h.deps) service.initialize() service.initialize() expect(h.listenerCount()).toBe(1) service.dispose() expect(h.listenerCount()).toBe(0) }) })