fix(desktop): harden session, meeting, caption and LLM lifecycles; route LLM calls through the gateway
This commit is contained in:
parent
3a46437f28
commit
ddc78546f0
62 changed files with 4786 additions and 648 deletions
132
apps/desktop/tests/red/llm-privacy-routing-redteam-r2-1.test.ts
Normal file
132
apps/desktop/tests/red/llm-privacy-routing-redteam-r2-1.test.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
// 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')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue