import { describe, it, expect, vi } from 'vitest' import { ErrorCode } from '@d3ro/core/errors' import { Feature } from '@d3ro/core/types' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getHistoryService } from '../../src/main/services/HistoryService' import { getLicenseService } from '../../src/main/services/LicenseService' import { getAutoLaunchService } from '../../src/main/services/AutoLaunchService' import { getCaptionService } from '../../src/main/services/CaptionService' import { configGet, configSet } from '../../src/main/services/ConfigService' import { registerLLMHandlers } from '../../src/main/ipc/llm-handlers' import { registerHistoryHandlers } from '../../src/main/ipc/history-handlers' import { registerLicenseHandlers } from '../../src/main/ipc/license-handlers' import { historyInput, invokeIpc, useRedHarness } from './harness' import { FX, USER_TEXT } from './fixtures' useRedHarness() describe('유스케이스: 사일런트 에러 / 폴백 성공 위장 탐지', () => { it('자동 제목 LLM 실패 후 히스토리는 completed 로 남되 title 공백을 성공 제목으로 쓰지 않는다', async () => { const entry = getHistoryService().create(historyInput({ originalText: USER_TEXT.KO })) const title = await getHistoryService().generateTitle(entry.id) expect(title === null || title.length > 0).toBe(true) if (title === null) { expect(getHistoryService().getById(entry.id)?.title ?? null).toBeNull() } }) it('라이선스 사용량 DB 가 열려 있을 때 getUsage 가 실패를 0으로 숨기지 않는다', () => { const svc = getLicenseService() svc.initialize() const usage = svc.getUsage(Feature.DICTATION) expect(usage.used).toBe(0) expect(usage.limit).toBe(-1) }) it('자동 시작 OS API 실패 시 config 와 OS 가 어긋난 채 성공으로 끝나지 않는다', () => { const errors: string[] = [] const orig = console.error try { getAutoLaunchService().setEnabled(true) expect(configGet('autoLaunch')).toBe(true) } catch (err) { errors.push(String(err)) expect(configGet('autoLaunch')).not.toBe(true) } void orig }) it('캡션 초기 상태는 inactive 이며 start 실패가 active+빈세그먼트로 위장되지 않는다', async () => { expect(getCaptionService().getState()).toBe('inactive') try { await getCaptionService().start() expect(['starting', 'active', 'inactive']).toContain(getCaptionService().getState()) } catch (err) { expect(getCaptionService().getState()).toBe('inactive') expect(err).toBeTruthy() } }) it('IPC llm:process 가 원문을 processedText 로 그대로 돌려주며 success 하지 않는다 (미인증)', 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) if (res.success) { expect(res.data.processedText).not.toBe(USER_TEXT.KO) } }) it('IPC 히스토리 삭제가 없는 id 를 success 로 위장하지 않는다', async () => { registerHistoryHandlers() const res = await invokeIpc(IPC_CHANNELS.HISTORY.DELETE, { id: 'ghost' }) expect(res.success).toBe(false) }) it('프리미엄 상태 IPC 가 로그인 없이 isAvailable:true 로 위장하지 않는다', async () => { 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 활성화가 tier=pro 로 위장하지 않는다', async () => { getLicenseService().initialize() registerLicenseHandlers() const res = await invokeIpc(IPC_CHANNELS.LICENSE.ACTIVATE, { licenseKey: '' }) expect(res.success).toBe(true) if (res.success) { expect(res.data.success).toBe(false) expect(res.data.tier).toBe('free') } expect(getLicenseService().tier).toBe('free') }) it('잘못된 키 활성화 메시지가 비어 있지 않다', async () => { const result = await getLicenseService().activate(FX.LICENSE_BAD) expect(result.success).toBe(false) expect(result.message.length).toBeGreaterThan(0) }) it('configSet 후 같은 키 get 이 이전 기본값을 반환하지 않는다', () => { configSet('soundEnabled', false) expect(configGet('soundEnabled')).toBe(false) }) it('authToken 만료 클리어 후 Online 경로가 캐시된 성공을 내지 않는다', async () => { configSet('authToken', FX.AUTH_TOKEN) configSet('llmBackend', 'online') registerLLMHandlers() vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: false, status: 401, json: async () => ({}) })), ) const res = await invokeIpc(IPC_CHANNELS.LLM.PROCESS, { text: 'x', action: 'refine' }) expect(res.success).toBe(false) expect(configGet('authToken')).toBeNull() }) it('캡션 setConfig 후 getConfig 가 반영된다', () => { getCaptionService().setConfig({ fontSize: 22 }) expect(getCaptionService().getConfig().fontSize).toBe(22) }) it('캡션 stop 은 inactive 에서 던져도 상태가 깨지지 않는다', async () => { await getCaptionService().stop() expect(getCaptionService().getState()).toBe('inactive') }) })