136 lines
6.3 KiB
TypeScript
136 lines
6.3 KiB
TypeScript
// 설정 동기화 회귀 — 데스크톱 전용 언어·프리셋 명령이 다음 pull에서 서버 값으로 되돌아가지 않는다.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createTestDb } from '../../helpers/createTestDb'
|
|
import { FakeSyncRemote } from '../../helpers/fakeSyncRemote'
|
|
import { bindTestDatabase, unbindTestDatabase } from '../../../src/main/db'
|
|
import { configGet, configSet, initInMemoryConfig, resetInMemoryConfig } from '../../../src/main/services/ConfigService'
|
|
import {
|
|
getCustomInstructionService,
|
|
resetCustomInstructionServiceForTests,
|
|
} from '../../../src/main/services/CustomInstructionService'
|
|
import { resetDictationTemplateServiceForTests } from '../../../src/main/services/DictationTemplateService'
|
|
import { resetMeetingDocTemplateServiceForTests } from '../../../src/main/services/MeetingDocTemplateService'
|
|
import { resetRAGServiceForTests } from '../../../src/main/services/RAGService'
|
|
import { SyncEngine } from '../../../src/main/services/sync/SyncEngine'
|
|
import { enqueueChange } from '../../../src/main/services/sync/sync-outbox'
|
|
import {
|
|
remoteToLocalPatch,
|
|
resolveActiveInstructionPush,
|
|
toRemoteSettings,
|
|
type LocalSettings,
|
|
} from '../../../src/main/services/sync/settings-sync-policy'
|
|
|
|
const USER = '11111111-1111-4111-8111-111111111111'
|
|
const U = '22222222-2222-4222-8222-222222222222'
|
|
const U2 = '33333333-3333-4333-8333-333333333333'
|
|
|
|
function local(overrides: Partial<LocalSettings> = {}): LocalSettings {
|
|
return { language: 'ko', theme: 'auto', defaultLLMAction: 'refine', activeInstructionId: '', ...overrides }
|
|
}
|
|
|
|
describe('settings-sync-policy: 언어', () => {
|
|
it('데스크톱 전용 언어(ja)는 서버의 ko/en이 덮지 않는다', () => {
|
|
const row = { locale: 'ko', theme_mode: 'system', auto_polish_enabled: true, active_instruction_id: null }
|
|
expect(remoteToLocalPatch(row, local({ language: 'ja' }), new Set())).toEqual({})
|
|
expect(remoteToLocalPatch({ ...row, locale: 'en' }, local({ language: 'zh-TW' }), new Set())).toEqual({})
|
|
})
|
|
|
|
it('로컬이 ko/en이면 서버 언어를 받는다', () => {
|
|
const row = { locale: 'en', theme_mode: 'system', auto_polish_enabled: true, active_instruction_id: null }
|
|
expect(remoteToLocalPatch(row, local({ language: 'ko' }), new Set())).toEqual({ language: 'en' })
|
|
})
|
|
|
|
it('서버가 모르는 locale 값은 무시한다', () => {
|
|
const row = { locale: 'fr', theme_mode: 'system', auto_polish_enabled: true, active_instruction_id: null }
|
|
expect(remoteToLocalPatch(row, local({ language: 'ko' }), new Set())).toEqual({})
|
|
})
|
|
|
|
it('push는 ko/en만 locale로 보낸다', () => {
|
|
expect(toRemoteSettings(local({ language: 'en' }))).toMatchObject({ locale: 'en' })
|
|
expect(toRemoteSettings(local({ language: 'ja' }))).not.toHaveProperty('locale')
|
|
})
|
|
})
|
|
|
|
describe('settings-sync-policy: 활성 명령', () => {
|
|
const ids = new Set([U, U2])
|
|
|
|
it('pull: 서버의 사용자 명령은 로컬 사용자 명령·없음을 바꾼다', () => {
|
|
expect(remoteToLocalPatch({ active_instruction_id: U }, local({ activeInstructionId: '' }), ids)).toEqual({
|
|
activeInstructionId: U,
|
|
})
|
|
expect(remoteToLocalPatch({ active_instruction_id: null }, local({ activeInstructionId: U }), ids)).toEqual({
|
|
activeInstructionId: '',
|
|
})
|
|
expect(remoteToLocalPatch({ active_instruction_id: 'not-local' }, local({ activeInstructionId: U }), ids)).toEqual({})
|
|
})
|
|
|
|
it('push: 없음·사용자 명령은 서버와 다를 때만 올린다', () => {
|
|
expect(resolveActiveInstructionPush('', U, ids)).toEqual({ instructionId: null })
|
|
expect(resolveActiveInstructionPush('', null, ids)).toBeNull()
|
|
expect(resolveActiveInstructionPush(U, U2, ids)).toEqual({ instructionId: U })
|
|
expect(resolveActiveInstructionPush(U, U, ids)).toBeNull()
|
|
})
|
|
|
|
it('push: 데스크톱 프리셋은 서버의 알려진 사용자 명령을 해제하고, 모르는 값은 건드리지 않는다', () => {
|
|
expect(resolveActiveInstructionPush('builtin-summarize', U, ids)).toEqual({ instructionId: null })
|
|
expect(resolveActiveInstructionPush('builtin-summarize', null, ids)).toBeNull()
|
|
expect(resolveActiveInstructionPush('builtin-summarize', '44444444-4444-4444-8444-444444444444', ids)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('설정 동기화 왕복 (엔진)', () => {
|
|
let testDb: ReturnType<typeof createTestDb>
|
|
let remote: FakeSyncRemote
|
|
let engine: SyncEngine
|
|
|
|
beforeEach(() => {
|
|
testDb = createTestDb()
|
|
bindTestDatabase(testDb.db, USER)
|
|
initInMemoryConfig()
|
|
resetCustomInstructionServiceForTests()
|
|
resetDictationTemplateServiceForTests()
|
|
resetMeetingDocTemplateServiceForTests()
|
|
resetRAGServiceForTests()
|
|
getCustomInstructionService().initialize()
|
|
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('fetch failed'))
|
|
remote = new FakeSyncRemote(USER)
|
|
engine = new SyncEngine({ remote, userId: USER })
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
engine.dispose()
|
|
unbindTestDatabase()
|
|
resetInMemoryConfig()
|
|
testDb.close()
|
|
})
|
|
|
|
it('데스크톱에서 ja를 고르면 이후 pull이 ko로 되돌리지 않는다', async () => {
|
|
remote.mobileInsert('user_settings', { locale: 'ko', theme_mode: 'system', auto_polish_enabled: true, revision: 1, active_instruction_id: null })
|
|
await engine.runFullSync()
|
|
configSet('language', 'ja')
|
|
enqueueChange('user_settings', 'self', 'upsert')
|
|
await engine.flush()
|
|
await engine.pull()
|
|
expect(configGet('language')).toBe('ja')
|
|
expect(remote.rows('user_settings')[0].locale).toBe('ko')
|
|
})
|
|
|
|
it('프리셋을 고르면 서버 활성 명령이 해제되어 다음 pull이 사용자 명령으로 되돌리지 않는다', async () => {
|
|
const command = getCustomInstructionService().create({ name: 'Calm', description: '', prompt: 'Make it calm' })
|
|
configSet('activeInstructionId', command.id)
|
|
await engine.runFullSync()
|
|
enqueueChange('user_settings', 'self', 'upsert')
|
|
await engine.flush()
|
|
expect(remote.rows('user_settings')[0].active_instruction_id).toBe(command.id)
|
|
|
|
configSet('activeInstructionId', 'builtin-summarize')
|
|
enqueueChange('user_settings', 'self', 'upsert')
|
|
await engine.flush()
|
|
expect(remote.rows('user_settings')[0].active_instruction_id).toBeNull()
|
|
|
|
await engine.pull()
|
|
expect(configGet('activeInstructionId')).toBe('builtin-summarize')
|
|
})
|
|
})
|