import { describe, it, expect } from 'vitest' import { ErrorCode } from '@d3ro/core/errors' import { Feature } from '@d3ro/core/types' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getLicenseService } from '../../src/main/services/LicenseService' import { registerLicenseHandlers } from '../../src/main/ipc/license-handlers' import { invokeIpc, useRedHarness } from './harness' import { FX } from './fixtures' useRedHarness() describe('유스케이스: 라이선스 활성화 / 기능 게이트 / 쿼터 / IPC', () => { it('초기 티어는 free 이다', () => { const svc = getLicenseService() svc.initialize() expect(svc.getInfo().tier).toBe('free') expect(svc.tier).toBe('free') }) it('빈 키 활성화는 실패하고 free 를 유지한다', async () => { const svc = getLicenseService() svc.initialize() const result = await svc.activate(' ') expect(result.success).toBe(false) expect(svc.tier).toBe('free') }) it('잘못된 키 활성화는 실패한다', async () => { const svc = getLicenseService() svc.initialize() const result = await svc.activate(FX.LICENSE_BAD) expect(result.success).toBe(false) expect(svc.getInfo().licenseKey).toBeNull() }) it('프로 키 활성화는 성공하고 tier 가 pro 가 된다', async () => { const svc = getLicenseService() svc.initialize() const result = await svc.activate(FX.LICENSE_PRO) expect(result.success).toBe(true) expect(result.tier).toBe('pro') expect(svc.tier).toBe('pro') expect(svc.getInfo().licenseKey).toBe(FX.LICENSE_PRO) expect(svc.getInfo().activatedAt).toBeGreaterThan(0) }) it('플러스 키 활성화는 pro_plus 가 된다', async () => { const svc = getLicenseService() svc.initialize() const result = await svc.activate(FX.LICENSE_PLUS) expect(result.success).toBe(true) expect(svc.tier).toBe('pro_plus') }) it('비활성화하면 free 로 돌아간다', async () => { const svc = getLicenseService() svc.initialize() await svc.activate(FX.LICENSE_PRO) await svc.deactivate() expect(svc.tier).toBe('free') expect(svc.getInfo().licenseKey).toBeNull() }) it('로컬 받아쓰기는 익명 free 에서도 허용된다', () => { const svc = getLicenseService() svc.initialize() expect(svc.canUse(Feature.DICTATION).allowed).toBe(true) expect(svc.canUse(Feature.DICTATION).reason).toBe('ok') }) it('클라우드 PREMIUM_LLM 은 로컬 모드에서 login_required 이다', () => { const svc = getLicenseService() svc.initialize() const access = svc.canUse(Feature.PREMIUM_LLM) expect(access.allowed).toBe(false) expect(access.reason).toBe('login_required') }) it('클라우드 CLOUD_SYNC 도 로컬 모드에서 login_required 이다', () => { const svc = getLicenseService() svc.initialize() expect(svc.canUse(Feature.CLOUD_SYNC).reason).toBe('login_required') }) it('login_required 기능을 consumeQuota 하면 에러가 나고 upgrade-prompt 가 뜬다', () => { const svc = getLicenseService() svc.initialize() const prompts: unknown[] = [] svc.on('upgrade-prompt', (e) => prompts.push(e)) expect(() => svc.consumeQuota(Feature.PREMIUM_LLM)).toThrow() try { svc.consumeQuota(Feature.CLOUD_SYNC) } catch (err) { expect((err as { code: number }).code).toBe(ErrorCode.TierRequired) } expect(prompts.length).toBeGreaterThan(0) }) it('로컬 기능 consumeQuota 는 성공한다', () => { const svc = getLicenseService() svc.initialize() expect(() => svc.consumeQuota(Feature.DICTATION)).not.toThrow() }) it('쿼터 없는 기능 getUsage 는 무제한(-1) 이다', () => { const svc = getLicenseService() svc.initialize() const usage = svc.getUsage(Feature.DICTATION) expect(usage.limit).toBe(-1) expect(usage.remaining).toBe(-1) }) it('syncFromCloud 는 티어를 바꾸고 tier-changed 를 낸다', () => { const svc = getLicenseService() svc.initialize() const events: string[] = [] svc.on('tier-changed', (info) => events.push(info.tier)) svc.syncFromCloud('pro') expect(svc.tier).toBe('pro') expect(events).toContain('pro') }) it('같은 티어 syncFromCloud 는 이벤트를 내지 않는다', () => { const svc = getLicenseService() svc.initialize() let count = 0 svc.on('tier-changed', () => { count += 1 }) svc.syncFromCloud('free') expect(count).toBe(0) }) it('resetToFree 는 pro 를 free 로 내린다', () => { const svc = getLicenseService() svc.initialize() svc.syncFromCloud('pro') svc.resetToFree() expect(svc.tier).toBe('free') }) it('getTierComparison 은 비어 있지 않다', () => { const svc = getLicenseService() svc.initialize() const rows = svc.getTierComparison() expect(rows.length).toBeGreaterThan(5) expect(rows.every((r) => r.featureLabel.length > 0)).toBe(true) }) it('IPC license:getInfo 는 free 를 돌려준다', async () => { getLicenseService().initialize() registerLicenseHandlers() const res = await invokeIpc(IPC_CHANNELS.LICENSE.GET_INFO) expect(res.success).toBe(true) if (res.success) expect(res.data.tier).toBe('free') }) it('IPC license:activate 잘못된 키는 성공 플래그가 false 다', async () => { getLicenseService().initialize() registerLicenseHandlers() const res = await invokeIpc(IPC_CHANNELS.LICENSE.ACTIVATE, { licenseKey: FX.LICENSE_BAD }) expect(res.success).toBe(true) if (res.success) expect(res.data.success).toBe(false) }) it('IPC license:activate 프로 키 후 checkFeature dictation 은 허용', async () => { getLicenseService().initialize() registerLicenseHandlers() const act = await invokeIpc(IPC_CHANNELS.LICENSE.ACTIVATE, { licenseKey: FX.LICENSE_PRO }) expect(act.success).toBe(true) const check = await invokeIpc(IPC_CHANNELS.LICENSE.CHECK_FEATURE, { feature: Feature.DICTATION }) expect(check.success).toBe(true) if (check.success) expect(check.data.allowed).toBe(true) }) it('IPC license:deactivate 후 티어는 free', async () => { getLicenseService().initialize() registerLicenseHandlers() await invokeIpc(IPC_CHANNELS.LICENSE.ACTIVATE, { licenseKey: FX.LICENSE_PRO }) const res = await invokeIpc(IPC_CHANNELS.LICENSE.DEACTIVATE) expect(res.success).toBe(true) expect(getLicenseService().tier).toBe('free') }) it('IPC license:getTierComparison 는 배열이다', async () => { getLicenseService().initialize() registerLicenseHandlers() const res = await invokeIpc(IPC_CHANNELS.LICENSE.GET_TIER_COMPARISON) expect(res.success).toBe(true) if (res.success) expect(Array.isArray(res.data)).toBe(true) }) })