feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View file

@ -0,0 +1,194 @@
import { describe, it, expect, vi } from 'vitest'
import { EventEmitter } from 'events'
import { ErrorCode } from '@d3ro/core/errors'
import { getVoiceConversationService } from '../../src/main/services/VoiceConversationService'
import { getVoiceActionService } from '../../src/main/services/VoiceActionService'
import { useRedHarness } from './harness'
import { FX, USER_TEXT } from './fixtures'
const audioBus = new EventEmitter()
vi.mock('../../src/main/services/AudioCaptureService', () => ({
getAudioCaptureService: () => ({
start: vi.fn(async () => undefined),
stop: vi.fn(async () => undefined),
on: (ev: string, fn: (...args: unknown[]) => void) => audioBus.on(ev, fn),
off: (ev: string, fn: (...args: unknown[]) => void) => audioBus.off(ev, fn),
}),
calculateRMS: () => 0,
}))
vi.mock('../../src/main/services/LocalSTTService', () => ({
getLocalSTTService: () => ({
initialize: vi.fn(async () => undefined),
transcribe: vi.fn(async () => ({
text: FX.STT_OK,
segments: [],
language: 'ko',
duration: 1,
processingTime: 1,
})),
getModels: () => [],
}),
resetLocalSTTServiceForTests: () => undefined,
}))
vi.mock('../../src/main/services/PremiumLLMService', () => ({
getPremiumLLMService: () => ({
processText: vi.fn(async () => FX.LLM_OK),
generate: vi.fn(async () => ({ text: FX.LLM_OK })),
chatStream: async function* () {
yield FX.LLM_OK
return FX.LLM_OK
},
cancelGeneration: vi.fn(),
isAvailable: () => false,
}),
resetPremiumLLMServiceForTests: () => undefined,
}))
vi.mock('../../src/main/services/LocalLLMService', () => ({
getLocalLLMService: () => ({
isAvailable: () => true,
processText: vi.fn(async () => FX.LLM_OK),
chatStream: async function* () {
yield FX.LLM_OK
return FX.LLM_OK
},
cancelGeneration: vi.fn(),
getStatus: () => ({
connectionState: 'connected',
serverUrl: 'http://localhost:11434',
activeModel: 'x',
serverVersion: null,
}),
}),
resetLocalLLMServiceForTests: () => undefined,
}))
vi.mock('../../src/main/services/TTSPlaybackService', () => ({
getTTSPlaybackService: () => ({
speak: vi.fn(async (text: string) => {
if (!text.trim()) {
const { D3ROError, ErrorCode } = await import('@d3ro/core/errors')
throw new D3ROError(ErrorCode.TTSTextEmpty, 'Empty TTS text')
}
}),
speakSentences: vi.fn(async () => undefined),
stop: vi.fn(),
isSpeaking: false,
on: vi.fn(),
off: vi.fn(),
removeAllListeners: vi.fn(),
}),
resetTTSPlaybackServiceForTests: () => undefined,
}))
vi.mock('../../src/main/services/SoundEffectService', () => ({
getSoundEffectService: () => ({ play: vi.fn() }),
}))
vi.mock('../../src/main/services/LicenseService', () => ({
getLicenseService: () => ({
canUse: () => ({ allowed: true, reason: 'ok' }),
promptUpgrade: vi.fn(),
}),
resetLicenseServiceForTests: () => undefined,
}))
useRedHarness()
describe('유스케이스: 음성 대화 세션 / 텍스트 전송 / 히스토리', () => {
it('초기 대화 상태는 idle 이다', () => {
const svc = getVoiceConversationService()
expect(svc.state).toBe('idle')
expect(svc.isActive).toBe(false)
expect(svc.getHistory()).toEqual([])
})
it('세션을 시작하면 listening 이다', async () => {
const svc = getVoiceConversationService()
await svc.startSession()
expect(svc.isActive).toBe(true)
expect(svc.state).toBe('listening')
svc.stopSession()
})
it('중복 시작은 ConversationSessionAlreadyActive 다', async () => {
const svc = getVoiceConversationService()
await svc.startSession()
await expect(svc.startSession()).rejects.toMatchObject({
code: ErrorCode.ConversationSessionAlreadyActive,
})
svc.stopSession()
})
it('세션 없이 텍스트 전송은 ConversationNoActiveSession 다', async () => {
await expect(getVoiceConversationService().sendTextMessage('hi')).rejects.toMatchObject({
code: ErrorCode.ConversationNoActiveSession,
})
})
it('텍스트 메시지를 보내면 히스토리에 user 가 남는다', async () => {
const svc = getVoiceConversationService()
await svc.startSession()
await svc.sendTextMessage(USER_TEXT.KO)
const hist = svc.getHistory()
expect(hist.some((m) => m.role === 'user' && m.content === USER_TEXT.KO)).toBe(true)
svc.stopSession()
})
it('히스토리를 비운다', async () => {
const svc = getVoiceConversationService()
await svc.startSession()
await svc.sendTextMessage(USER_TEXT.EN)
svc.clearHistory()
expect(svc.getHistory()).toEqual([])
svc.stopSession()
})
it('세션을 중지하면 idle 이다', async () => {
const svc = getVoiceConversationService()
await svc.startSession()
svc.stopSession()
expect(svc.isActive).toBe(false)
expect(svc.state).toBe('idle')
})
it('비활성 중지/취소는 던지지 않는다', () => {
expect(() => getVoiceConversationService().stopSession()).not.toThrow()
expect(() => getVoiceConversationService().cancelResponse()).not.toThrow()
})
it('프리미엄 불가 시 로컬 LLM 경로로 텍스트가 히스토리에 남는다', async () => {
const svc = getVoiceConversationService()
await svc.startSession()
await svc.sendTextMessage(USER_TEXT.KO)
const hist = svc.getHistory()
expect(hist.some((m) => m.role === 'user' && m.content === USER_TEXT.KO)).toBe(true)
expect(hist.some((m) => m.role === 'assistant' && m.content === FX.LLM_OK)).toBe(true)
svc.stopSession()
})
})
describe('유스케이스: 보이스 액션', () => {
it('초기 액션은 비활성이다', () => {
expect(getVoiceActionService().isEnabled).toBe(false)
})
it('활성 토글이 동작한다', () => {
getVoiceActionService().setEnabled(true)
expect(getVoiceActionService().isEnabled).toBe(true)
getVoiceActionService().setEnabled(false)
expect(getVoiceActionService().isEnabled).toBe(false)
})
it('프리셋 목록이 비어 있지 않다', () => {
expect(getVoiceActionService().getPresets().length).toBeGreaterThan(0)
})
it('히스토리를 비운다', () => {
getVoiceActionService().clearHistory()
expect(getVoiceActionService().getHistory()).toEqual([])
})
})