// tests/unit/stt-tab-subscriptions-redteam-r1-8.test.ts // STT 탭 언마운트 시 모든 진행률 구독이 해제되는지 (runtime:progress 리스너 누수 회귀). import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { join } from 'node:path' import { subscribeSttTabEvents } from '../../src/renderer/components/stt/sttTabSubscriptions' type Listener = (event: number) => void function fakeChannel(): { subscribe: (cb: Listener) => () => void; listeners: Set } { const listeners = new Set() return { listeners, subscribe: (cb) => { listeners.add(cb) return () => { listeners.delete(cb) } }, } } describe('subscribeSttTabEvents', () => { it('마운트·언마운트를 반복해도 리스너가 쌓이지 않는다', () => { const download = fakeChannel() const runtime = fakeChannel() for (let mount = 0; mount < 12; mount += 1) { const cleanup = subscribeSttTabEvents( { onDownloadProgress: download.subscribe, onRuntimeProgress: runtime.subscribe }, { onDownloadProgress: () => undefined, onRuntimeProgress: () => undefined }, ) expect(download.listeners.size).toBe(1) expect(runtime.listeners.size).toBe(1) cleanup() } expect(download.listeners.size).toBe(0) expect(runtime.listeners.size).toBe(0) }) it('cleanup 을 두 번 불러도 안전하다 (StrictMode 이중 호출)', () => { const download = fakeChannel() const runtime = fakeChannel() const cleanup = subscribeSttTabEvents( { onDownloadProgress: download.subscribe, onRuntimeProgress: runtime.subscribe }, { onDownloadProgress: () => undefined, onRuntimeProgress: () => undefined }, ) cleanup() expect(() => cleanup()).not.toThrow() }) it('STTTab 은 진행률 이벤트를 이 헬퍼로만 구독한다 (개별 구독 + 누락된 해제 재발 방지)', () => { const source = readFileSync( join(__dirname, '../../src/renderer/components/STTTab.tsx'), 'utf8', ) expect(source).toContain('return subscribeSttTabEvents(') expect(source).not.toMatch(/const\s+unsub\w*\s*=\s*window\.electronAPI\.(stt|runtime)\.on/) }) })