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
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:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
252
apps/desktop/tests/red/stt-llm.usecase.test.ts
Normal file
252
apps/desktop/tests/red/stt-llm.usecase.test.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { ErrorCode } from '@d3ro/core/errors'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import { getCloudSTTService } from '../../src/main/services/CloudSTTService'
|
||||
import { getPremiumLLMService } from '../../src/main/services/PremiumLLMService'
|
||||
import { getOnlineLLMService } from '../../src/main/services/OnlineLLMService'
|
||||
import { getLocalLLMService } from '../../src/main/services/LocalLLMService'
|
||||
import { configSet } from '../../src/main/services/ConfigService'
|
||||
import { registerSTTHandlers } from '../../src/main/ipc/stt-handlers'
|
||||
import { registerLLMHandlers } from '../../src/main/ipc/llm-handlers'
|
||||
import { FX, USER_TEXT } from './fixtures'
|
||||
import { invokeIpc, useRedHarness } from './harness'
|
||||
|
||||
const cloudPorts = {
|
||||
authenticated: true,
|
||||
invoke: async (
|
||||
_name: string,
|
||||
_body: Record<string, unknown>,
|
||||
): Promise<{ data: unknown; error: { message: string } | null }> => ({
|
||||
data: null,
|
||||
error: { message: 'fx.default.unconfigured' },
|
||||
}),
|
||||
}
|
||||
|
||||
vi.mock('../../src/main/services/CloudSyncService', () => ({
|
||||
getCloudSyncService: () => ({
|
||||
isEnabled: () => true,
|
||||
isAuthenticated: () => cloudPorts.authenticated,
|
||||
invokeFunction: (name: string, body: Record<string, unknown>) => cloudPorts.invoke(name, body),
|
||||
signInAnonymously: async () => {
|
||||
throw new Error('fx.anon.not-supported')
|
||||
},
|
||||
pushOne: async () => undefined,
|
||||
getState: () => ({
|
||||
authenticated: cloudPorts.authenticated,
|
||||
userEmail: null,
|
||||
lastSyncAt: null,
|
||||
syncing: false,
|
||||
}),
|
||||
}),
|
||||
resetCloudSyncServiceForTests: () => undefined,
|
||||
}))
|
||||
|
||||
useRedHarness()
|
||||
|
||||
describe('유스케이스: STT/LLM 모델 I/O 실패·빈값·기형 응답', () => {
|
||||
it('미인증 STT initialize 는 조용히 ready 가 되지 않고 이후 transcribe 가 실패한다', async () => {
|
||||
cloudPorts.authenticated = false
|
||||
await getCloudSTTService().initialize()
|
||||
await expect(
|
||||
getCloudSTTService().transcribe(Buffer.alloc(100)),
|
||||
).rejects.toMatchObject({ code: ErrorCode.STTTranscriptionFailed })
|
||||
})
|
||||
|
||||
it('STT 프록시 에러는 STTTranscriptionFailed 다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({ data: null, error: { message: 'fx.stt.proxy-down' } })
|
||||
await expect(getCloudSTTService().transcribe(Buffer.from('pcm'))).rejects.toMatchObject({
|
||||
code: ErrorCode.STTTranscriptionFailed,
|
||||
})
|
||||
})
|
||||
|
||||
it('STT 빈 텍스트는 성공 객체가 아니라 에러다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({
|
||||
data: { text: FX.STT_EMPTY, segments: [], language: 'ko' },
|
||||
error: null,
|
||||
})
|
||||
await expect(getCloudSTTService().transcribe(Buffer.from('pcm'))).rejects.toMatchObject({
|
||||
code: ErrorCode.STTNoAudioData,
|
||||
})
|
||||
})
|
||||
|
||||
it('STT 기형 페이로드(text 없음)는 파싱 실패다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({ data: FX.STT_MALFORMED_SHAPE, error: null })
|
||||
await expect(getCloudSTTService().transcribe(Buffer.from('pcm'))).rejects.toMatchObject({
|
||||
code: ErrorCode.STTTranscriptionFailed,
|
||||
})
|
||||
})
|
||||
|
||||
it('STT 정상 픽스처는 text 가 픽스처 토큰이다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({
|
||||
data: { text: FX.STT_OK, segments: [], language: 'ko', duration: 1, processingTime: 1 },
|
||||
error: null,
|
||||
})
|
||||
const result = await getCloudSTTService().transcribe(Buffer.from('pcm'))
|
||||
expect(result.text).toBe(FX.STT_OK)
|
||||
expect(result.text).not.toBe(USER_TEXT.KO)
|
||||
})
|
||||
|
||||
it('미인증 PremiumLLM.processText 는 로그인 에러 + upgrade-required 다', async () => {
|
||||
cloudPorts.authenticated = false
|
||||
const llm = getPremiumLLMService()
|
||||
const events: unknown[] = []
|
||||
llm.on('upgrade-required', (e) => events.push(e))
|
||||
await expect(llm.processText(USER_TEXT.KO, 'refine')).rejects.toMatchObject({
|
||||
code: ErrorCode.LLMServerUnreachable,
|
||||
})
|
||||
expect(events.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('PremiumLLM 프록시 에러는 원문을 반환하지 않는다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({ data: null, error: { message: 'fx.llm.500' } })
|
||||
await expect(getPremiumLLMService().processText(USER_TEXT.KO, 'refine')).rejects.toMatchObject({
|
||||
code: ErrorCode.LLMProcessingFailed,
|
||||
})
|
||||
})
|
||||
|
||||
it('PremiumLLM 빈 content 는 원문 폴백이 아니라 에러다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({
|
||||
data: { content: [{ type: 'text', text: FX.LLM_EMPTY }] },
|
||||
error: null,
|
||||
})
|
||||
await expect(getPremiumLLMService().processText(USER_TEXT.KO, 'refine')).rejects.toMatchObject({
|
||||
code: ErrorCode.LLMProcessingFailed,
|
||||
})
|
||||
})
|
||||
|
||||
it('PremiumLLM 기형 content 는 에러다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({ data: { content: [] }, error: null })
|
||||
await expect(getPremiumLLMService().processText(USER_TEXT.KO, 'refine')).rejects.toBeTruthy()
|
||||
})
|
||||
|
||||
it('PremiumLLM 정상 픽스처는 입력과 다른 토큰이다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({
|
||||
data: { content: [{ type: 'text', text: FX.LLM_OK }] },
|
||||
error: null,
|
||||
})
|
||||
const out = await getPremiumLLMService().processText(USER_TEXT.KO, 'refine')
|
||||
expect(out).toBe(FX.LLM_OK)
|
||||
expect(out).not.toBe(USER_TEXT.KO)
|
||||
})
|
||||
|
||||
it('쿼터 초과 메시지는 upgrade-required 를 낸다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({ data: null, error: { message: 'quota_exceeded 429' } })
|
||||
const llm = getPremiumLLMService()
|
||||
const ev: unknown[] = []
|
||||
llm.on('upgrade-required', (e) => ev.push(e))
|
||||
await expect(llm.processText('x', 'refine')).rejects.toBeTruthy()
|
||||
expect(ev.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('generate() 가 공개 API 로 존재하고 픽스처를 반환한다', async () => {
|
||||
cloudPorts.authenticated = true
|
||||
cloudPorts.invoke = async () => ({
|
||||
data: { content: [{ type: 'text', text: FX.LLM_TITLE }] },
|
||||
error: null,
|
||||
})
|
||||
const llm = getPremiumLLMService() as { generate?: (t: string, o?: unknown) => Promise<{ text: string }> }
|
||||
expect(typeof llm.generate).toBe('function')
|
||||
const result = await llm.generate!(USER_TEXT.KO, { systemPrompt: 'title' })
|
||||
expect(result.text).toBe(FX.LLM_TITLE)
|
||||
})
|
||||
|
||||
it('IPC llm:process 실패는 success:false 다', async () => {
|
||||
cloudPorts.authenticated = false
|
||||
configSet('llmBackend', 'premium')
|
||||
registerLLMHandlers()
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.PROCESS, {
|
||||
text: USER_TEXT.KO,
|
||||
action: 'refine',
|
||||
})
|
||||
expect(res.success).toBe(false)
|
||||
if (!res.success) expect(res.error.code).toBe(ErrorCode.LLMProcessingFailed)
|
||||
})
|
||||
|
||||
it('IPC llm:process online + 미로그인은 실패다', async () => {
|
||||
configSet('llmBackend', 'online')
|
||||
configSet('authToken', null)
|
||||
registerLLMHandlers()
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.PROCESS, {
|
||||
text: USER_TEXT.KO,
|
||||
action: 'refine',
|
||||
})
|
||||
expect(res.success).toBe(false)
|
||||
})
|
||||
|
||||
it('IPC premium getStatus 는 실제 가용 여부를 반영한다 (항상 true 금지)', async () => {
|
||||
cloudPorts.authenticated = false
|
||||
registerLLMHandlers()
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.PREMIUM_GET_STATUS)
|
||||
expect(res.success).toBe(true)
|
||||
if (res.success) expect(res.data.isAvailable).toBe(false)
|
||||
})
|
||||
|
||||
it('IPC premium getQuota 는 하드코딩 999999 이 아니다', async () => {
|
||||
registerLLMHandlers()
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.PREMIUM_GET_QUOTA)
|
||||
expect(res.success).toBe(true)
|
||||
if (res.success) {
|
||||
expect(res.data.remainingTokens).not.toBe(999999)
|
||||
}
|
||||
})
|
||||
|
||||
it('IPC stt:setLanguage / getLanguage 왕복', async () => {
|
||||
registerSTTHandlers()
|
||||
await invokeIpc(IPC_CHANNELS.STT.SET_LANGUAGE, { language: 'en' })
|
||||
const res = await invokeIpc(IPC_CHANNELS.STT.GET_LANGUAGE)
|
||||
expect(res.success).toBe(true)
|
||||
if (res.success) expect(res.data).toBe('en')
|
||||
})
|
||||
|
||||
it('IPC llm:setModel / getActiveModel 왕복', async () => {
|
||||
registerLLMHandlers()
|
||||
await invokeIpc(IPC_CHANNELS.LLM.SET_MODEL, { modelId: 'fx-model-id' })
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.GET_ACTIVE_MODEL)
|
||||
expect(res.success).toBe(true)
|
||||
if (res.success) expect(res.data).toBe('fx-model-id')
|
||||
})
|
||||
|
||||
it('IPC llm:getStatus 는 LocalLLM.getStatus() 와 같고 하드코딩 ready 가 아니다', async () => {
|
||||
registerLLMHandlers()
|
||||
const live = getLocalLLMService().getStatus()
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.GET_STATUS)
|
||||
expect(res.success).toBe(true)
|
||||
if (res.success) {
|
||||
expect(res.data).toEqual(live)
|
||||
expect(res.data).not.toMatchObject({
|
||||
isInstalled: true,
|
||||
isRunning: true,
|
||||
isReady: true,
|
||||
currentModel: 'premium',
|
||||
})
|
||||
expect(live.connectionState === 'disconnected' || live.connectionState === 'connected').toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('IPC llm:pullModel 실패는 성공으로 위장하지 않는다', async () => {
|
||||
registerLLMHandlers()
|
||||
const res = await invokeIpc(IPC_CHANNELS.LLM.PULL_MODEL, { modelId: 'fx-missing-model' })
|
||||
expect(res.success).toBe(false)
|
||||
if (!res.success) expect(res.error.code).toBe(ErrorCode.LLMServerUnreachable)
|
||||
})
|
||||
|
||||
it('OnlineLLM 5xx 는 LLMProcessingFailed 다', async () => {
|
||||
configSet('authToken', FX.AUTH_TOKEN)
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(async () => ({ ok: false, status: 503, json: async () => ({}) })),
|
||||
)
|
||||
await expect(getOnlineLLMService().processText('x', 'refine')).rejects.toMatchObject({
|
||||
code: ErrorCode.LLMProcessingFailed,
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue