134 lines
5.4 KiB
TypeScript
134 lines
5.4 KiB
TypeScript
// tests/main/services/caption-owner-redteam-r1-3.test.ts
|
|
// CaptionService 세션 계약 회귀 테스트:
|
|
// - 회의 모드가 소유한 자막 세션은 사용자 요청(stop('user'))으로 멈추지 않는다
|
|
// - stop() 은 flush 중 확정된 마지막 문장까지 담은 요약을 돌려준다
|
|
// - persistHistory:false 면 'caption' 히스토리를 만들지 않고 session-saved 도 없다
|
|
// - 저장한 경우 요약에 히스토리 행 id 가 실린다 (자동 요약이 그 id 로 찾는다)
|
|
// - audioSource 옵션은 전역 설정/다음 세션으로 새지 않는다
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
|
|
vi.mock('../../../src/main/services/LoggerService', () => ({
|
|
getLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }),
|
|
}))
|
|
|
|
const config = vi.hoisted(() => ({ values: {} as Record<string, unknown> }))
|
|
vi.mock('../../../src/main/services/ConfigService', () => ({
|
|
configGet: vi.fn((key: string) => config.values[key]),
|
|
configSet: vi.fn((key: string, value: unknown) => {
|
|
config.values[key] = value
|
|
}),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/LocalSTTService', () => ({
|
|
getLocalSTTService: () => ({
|
|
initialize: vi.fn(async () => undefined),
|
|
ensureAuxModel: vi.fn(async () => undefined),
|
|
transcribe: vi.fn(async () => ({ text: '', segments: [], language: 'ko', duration: 0, processingTime: 0 })),
|
|
}),
|
|
}))
|
|
|
|
const audio = vi.hoisted(() => ({ start: vi.fn(async () => undefined), stop: vi.fn(async () => undefined) }))
|
|
vi.mock('../../../src/main/services/AudioCaptureService', () => ({
|
|
getAudioCaptureService: () => ({ ...audio, on: vi.fn(), off: vi.fn() }),
|
|
calculateRMS: () => 0.1,
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/SoundEffectService', () => ({
|
|
getSoundEffectService: () => ({ play: vi.fn() }),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/LocalLLMService', () => ({
|
|
getLocalLLMService: () => ({ isAvailable: () => false, generate: vi.fn() }),
|
|
}))
|
|
|
|
const history = vi.hoisted(() => ({ create: vi.fn() }))
|
|
vi.mock('../../../src/main/services/HistoryService', () => ({
|
|
getHistoryService: () => history,
|
|
}))
|
|
|
|
// 트랙: flush 하면 말하던 마지막 문장을 확정한다
|
|
vi.mock('../../../src/main/services/caption/StreamingCaptionTrack', () => ({
|
|
StreamingCaptionTrack: class {
|
|
constructor(private readonly deps: { onFinal: (text: string) => void }) {}
|
|
push(): void {}
|
|
async tick(): Promise<void> {}
|
|
async flush(): Promise<void> {
|
|
this.deps.onFinal('정지 직전 마지막 문장')
|
|
}
|
|
},
|
|
}))
|
|
|
|
type CaptionModule = typeof import('../../../src/main/services/CaptionService')
|
|
let mod: CaptionModule
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
config.values = { captionAudioSource: 'system' }
|
|
history.create.mockReturnValue({ id: 'history-row-1' })
|
|
mod = await import('../../../src/main/services/CaptionService')
|
|
mod.resetCaptionServiceForTests()
|
|
})
|
|
|
|
describe('CaptionService 세션 소유권', () => {
|
|
it('회의가 소유한 세션은 stop(\'user\') 를 거부하고 계속 돈다', async () => {
|
|
const caption = mod.getCaptionService()
|
|
await caption.start({ owner: 'meeting', audioSource: 'mic', showOverlay: false, persistHistory: false })
|
|
|
|
expect(await caption.stop('user')).toBe(false)
|
|
expect(caption.getState()).toBe('active')
|
|
expect(audio.stop).not.toHaveBeenCalled()
|
|
|
|
// 소유자와 강제 정지(인자 없음)는 멈출 수 있다
|
|
expect(await caption.stop('meeting')).not.toBe(false)
|
|
expect(caption.getState()).toBe('inactive')
|
|
})
|
|
|
|
it('사용자 세션은 stop(\'user\') 로 멈춘다', async () => {
|
|
const caption = mod.getCaptionService()
|
|
await caption.start()
|
|
expect(caption.getOwner()).toBe('user')
|
|
await caption.stop('user')
|
|
expect(caption.getState()).toBe('inactive')
|
|
})
|
|
})
|
|
|
|
describe('CaptionService 정지 요약', () => {
|
|
it('flush 중 확정된 문장을 요약에 담아 돌려주고, persistHistory:false 면 히스토리를 만들지 않는다', async () => {
|
|
const caption = mod.getCaptionService()
|
|
const saved = vi.fn()
|
|
caption.on('session-saved', saved)
|
|
await caption.start({ owner: 'meeting', audioSource: 'mic', showOverlay: false, persistHistory: false })
|
|
|
|
const summary = await caption.stop('meeting')
|
|
|
|
expect(summary).not.toBe(false)
|
|
expect(summary && summary.segments.map((s) => s.text)).toEqual(['정지 직전 마지막 문장'])
|
|
expect(summary && summary.historyId).toBeNull()
|
|
expect(history.create).not.toHaveBeenCalled()
|
|
expect(saved).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('저장한 세션은 요약에 히스토리 행 id 를 싣는다', async () => {
|
|
const caption = mod.getCaptionService()
|
|
const saved = vi.fn()
|
|
caption.on('session-saved', saved)
|
|
await caption.start({ audioSource: 'mic' })
|
|
|
|
const summary = await caption.stop()
|
|
|
|
expect(history.create).toHaveBeenCalledTimes(1)
|
|
expect(summary && summary.historyId).toBe('history-row-1')
|
|
expect(saved).toHaveBeenCalledWith(expect.objectContaining({ historyId: 'history-row-1' }))
|
|
})
|
|
|
|
it('audioSource 옵션은 다음 사용자 세션의 설정으로 새지 않는다', async () => {
|
|
const caption = mod.getCaptionService()
|
|
caption.setConfig({ audioSource: 'system' })
|
|
await caption.start({ owner: 'meeting', audioSource: 'mic', showOverlay: false, persistHistory: false })
|
|
await caption.stop()
|
|
expect(config.values.captionAudioSource).toBe('system')
|
|
expect(caption.getConfig().audioSource).toBe('system')
|
|
})
|
|
})
|