132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
// tests/red/llm-privacy-routing-redteam-r2-1.test.ts
|
|
// LLM 백엔드 선택이 게이트웨이 한 곳을 따른다(실제 서비스 + in-memory DB, LLM 어댑터만 가짜):
|
|
// - llmBackend='local' 이면 자막 자동 요약·받아쓰기 자동 제목이 Premium(클라우드·쿼터)을 부르지 않는다
|
|
// - llm:process 는 online + Supabase 로그인(레거시 authToken 없음)이면 Premium 을 탄다(.NET 경로 아님)
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
|
import { invokeIpc, useRedHarness, historyInput } from './harness'
|
|
|
|
const llm = vi.hoisted(() => ({
|
|
localGenerate: vi.fn(),
|
|
localProcess: vi.fn(),
|
|
premiumGenerate: vi.fn(),
|
|
premiumProcess: vi.fn(),
|
|
premiumAvailable: true,
|
|
onlineProcess: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../src/main/services/LocalLLMService', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../src/main/services/LocalLLMService')>()
|
|
return {
|
|
...actual,
|
|
getLocalLLMService: () => ({
|
|
isAvailable: () => true,
|
|
generate: (...args: unknown[]) => llm.localGenerate(...args),
|
|
processText: (...args: unknown[]) => llm.localProcess(...args),
|
|
chatStream: async function* () {
|
|
yield 'x'
|
|
return 'x'
|
|
},
|
|
cancelGeneration: vi.fn(),
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
getStatus: () => ({}),
|
|
}),
|
|
}
|
|
})
|
|
|
|
vi.mock('../../src/main/services/PremiumLLMService', () => ({
|
|
getPremiumLLMService: () => ({
|
|
isAvailable: () => llm.premiumAvailable,
|
|
generate: (...args: unknown[]) => llm.premiumGenerate(...args),
|
|
processText: (...args: unknown[]) => llm.premiumProcess(...args),
|
|
chatStream: async function* () {
|
|
yield 'p'
|
|
return 'p'
|
|
},
|
|
cancelGeneration: vi.fn(),
|
|
on: vi.fn(),
|
|
off: vi.fn(),
|
|
getLastQuota: () => null,
|
|
}),
|
|
resetPremiumLLMServiceForTests: () => undefined,
|
|
}))
|
|
|
|
vi.mock('../../src/main/services/OnlineLLMService', () => ({
|
|
getOnlineLLMService: () => ({
|
|
processText: (...args: unknown[]) => llm.onlineProcess(...args),
|
|
cancelGeneration: vi.fn(),
|
|
}),
|
|
resetOnlineLLMServiceForTests: () => undefined,
|
|
}))
|
|
|
|
useRedHarness()
|
|
|
|
import { configSet } from '../../src/main/services/ConfigService'
|
|
import { getHistoryService } from '../../src/main/services/HistoryService'
|
|
import { getMeetingSummaryService } from '../../src/main/services/MeetingSummaryService'
|
|
import { registerLLMHandlers } from '../../src/main/ipc/llm-handlers'
|
|
import { setLlmGatewayForTests } from '../../src/main/services/llm/LlmGateway'
|
|
|
|
beforeEach(() => {
|
|
setLlmGatewayForTests(null)
|
|
llm.premiumAvailable = true
|
|
llm.localGenerate.mockReset().mockResolvedValue({ text: '## 요약\n로컬 요약', model: 'local' })
|
|
llm.localProcess.mockReset().mockResolvedValue('local-result')
|
|
llm.premiumGenerate.mockReset().mockResolvedValue({ text: 'premium' })
|
|
llm.premiumProcess.mockReset().mockResolvedValue('premium-result')
|
|
llm.onlineProcess.mockReset().mockResolvedValue('dotnet-result')
|
|
})
|
|
|
|
describe('llmBackend=local 은 텍스트를 클라우드로 보내지 않는다', () => {
|
|
it('자막 요약은 로컬 LLM 을 쓰고 Premium 을 부르지 않는다', async () => {
|
|
configSet('llmBackend', 'local')
|
|
const entry = getHistoryService().create(historyInput({ originalText: '회의 내용 '.repeat(20), mode: 'caption' }))
|
|
await vi.waitFor(() => expect(llm.localGenerate).toHaveBeenCalled())
|
|
llm.localGenerate.mockClear()
|
|
|
|
const result = await getMeetingSummaryService().summarize(entry.id)
|
|
|
|
expect(result.summary).toContain('로컬 요약')
|
|
expect(llm.localGenerate).toHaveBeenCalled()
|
|
expect(llm.premiumGenerate).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('받아쓰기 자동 제목도 Premium 을 부르지 않는다', async () => {
|
|
configSet('llmBackend', 'local')
|
|
const entry = getHistoryService().create(historyInput({ originalText: '다음 주 출시 일정을 정리해야 합니다' }))
|
|
await vi.waitFor(() => expect(llm.localGenerate).toHaveBeenCalled())
|
|
expect(llm.premiumGenerate).not.toHaveBeenCalled()
|
|
await vi.waitFor(() => expect(getHistoryService().getById(entry.id)?.title).toBeTruthy())
|
|
})
|
|
|
|
it('online 을 고른 사용자는 요약이 Premium 을 탄다(명시적 선택)', async () => {
|
|
configSet('llmBackend', 'online')
|
|
const entry = getHistoryService().create(historyInput({ originalText: '회의 내용 '.repeat(20), mode: 'caption' }))
|
|
await getMeetingSummaryService().summarize(entry.id)
|
|
expect(llm.premiumGenerate).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('llm:process 는 실제 받아쓰기와 같은 경로를 탄다', () => {
|
|
it('online + Supabase 로그인(레거시 authToken 없음) → Premium, .NET 경로 아님', async () => {
|
|
configSet('llmBackend', 'online')
|
|
configSet('authToken', null)
|
|
registerLLMHandlers()
|
|
const res = await invokeIpc<{ processedText: string }>(IPC_CHANNELS.LLM.PROCESS, { text: '원문', action: 'refine' })
|
|
expect(res.success).toBe(true)
|
|
if (res.success) expect(res.data.processedText).toBe('premium-result')
|
|
expect(llm.onlineProcess).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('online 인데 Premium 을 쓸 수 없으면 받아쓰기처럼 로컬로 폴백한다', async () => {
|
|
configSet('llmBackend', 'online')
|
|
configSet('authToken', null)
|
|
llm.premiumAvailable = false
|
|
registerLLMHandlers()
|
|
const res = await invokeIpc<{ processedText: string }>(IPC_CHANNELS.LLM.PROCESS, { text: '원문', action: 'refine' })
|
|
expect(res.success).toBe(true)
|
|
if (res.success) expect(res.data.processedText).toBe('local-result')
|
|
})
|
|
})
|