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
252 lines
9.7 KiB
TypeScript
252 lines
9.7 KiB
TypeScript
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,
|
|
})
|
|
})
|
|
})
|