// 회의 전사 구간(transcripts)과 프리셋 명령 프롬프트 동기화. import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { eq } from 'drizzle-orm' import { createTestDb } from '../../helpers/createTestDb' import { FakeSyncRemote } from '../../helpers/fakeSyncRemote' import { bindTestDatabase, unbindTestDatabase } from '../../../src/main/db' import { meetingSessions } from '../../../src/main/db/schema' import { initInMemoryConfig, resetInMemoryConfig } from '../../../src/main/services/ConfigService' import { getBuiltinInstructionDefaultPrompt, getCustomInstructionService, resetCustomInstructionServiceForTests, } from '../../../src/main/services/CustomInstructionService' import { resetDictationTemplateServiceForTests } from '../../../src/main/services/DictationTemplateService' import { resetMeetingDocTemplateServiceForTests } from '../../../src/main/services/MeetingDocTemplateService' import { SyncEngine } from '../../../src/main/services/sync/SyncEngine' import { enqueueChange, outboxCounts } from '../../../src/main/services/sync/sync-outbox' const USER = '11111111-1111-4111-8111-111111111111' let testDb: ReturnType let remote: FakeSyncRemote let engine: SyncEngine beforeEach(() => { testDb = createTestDb() bindTestDatabase(testDb.db, USER) initInMemoryConfig() resetCustomInstructionServiceForTests() resetDictationTemplateServiceForTests() resetMeetingDocTemplateServiceForTests() getCustomInstructionService().initialize() remote = new FakeSyncRemote(USER) engine = new SyncEngine({ remote, userId: USER }) }) afterEach(() => { engine.dispose() unbindTestDatabase() resetInMemoryConfig() testDb.close() }) function segmentsOf(meetingId: string): Array> { return remote .rows('transcripts') .filter((r) => r.meeting_id === meetingId) .sort((a, b) => Number(a.segment_index) - Number(b.segment_index)) } describe('회의 전사 구간', () => { it('데스크톱 전사를 구간으로 올리고, 수정·화자 구분을 반영하며 줄어든 구간은 지운다', async () => { const id = crypto.randomUUID() const now = Date.now() testDb.db .insert(meetingSessions) .values({ id, title: 'desk', status: 'completed', startedAt: now, rawTranscript: '[00:00] 시작합니다\n[00:07] 첫 안건\n[00:15] 마무리', createdAt: now, updatedAt: now, }) .run() await engine.runFullSync() expect(segmentsOf(id).map((s) => [s.segment_index, s.timestamp_ms, s.duration_ms, s.text, s.edited])).toEqual([ [0, 0, 7000, '시작합니다', false], [1, 7000, 8000, '첫 안건', false], [2, 15000, null, '마무리', false], ]) testDb.db .update(meetingSessions) .set({ editedTranscript: '[00:00] [화자 1] 시작합니다\n[00:15] [화자 2] 마무리', updatedAt: Date.now() }) .where(eq(meetingSessions.id, id)) .run() enqueueChange('meetings', id, 'upsert') await engine.flush() expect(segmentsOf(id).map((s) => [s.segment_index, s.speaker, s.text, s.edited])).toEqual([ [0, '화자 1', '시작합니다', true], [1, '화자 2', '마무리', true], ]) }) it('전사가 없는 회의(녹음 중)는 서버 구간을 건드리지 않는다', async () => { const id = crypto.randomUUID() remote.mobileInsert('meetings', { id, title: 'phone', status: 'completed', started_at: remote.now() }) remote.rows('transcripts').push({ id: crypto.randomUUID(), meeting_id: id, segment_index: 0, timestamp_ms: 0, text: 'phone text' }) await engine.runFullSync() testDb.db.update(meetingSessions).set({ title: 'renamed', rawTranscript: null, updatedAt: Date.now() }).where(eq(meetingSessions.id, id)).run() enqueueChange('meetings', id, 'upsert') await engine.flush() expect(segmentsOf(id).map((s) => s.text)).toEqual(['phone text']) }) }) describe('프리셋 명령 프롬프트', () => { it('데스크톱에서 고친 프리셋만 서버 프리셋 행으로 올리고, 번역 자리표시자는 English 로 바꾼다', async () => { const service = getCustomInstructionService() service.update('builtin-summarize', { prompt: '핵심만 두 줄로' }) service.update('builtin-translate', { prompt: '{{targetLanguage}}로 자연스럽게 번역' }) await engine.runFullSync() const byKey = (key: string) => remote.rows('custom_instructions').find((r) => r.builtin_key === key) expect(byKey('summarize')?.prompt).toBe('핵심만 두 줄로') expect(byKey('translate_en')?.prompt).toBe('English로 자연스럽게 번역') expect(byKey('formal')?.prompt).toBe(FakeSyncRemote.BUILTIN_DEFAULTS.formal) expect(outboxCounts().pending).toBe(0) // 다시 받아도 로컬 문구({{targetLanguage}})를 바꾸지 않는다 await engine.pull() expect(service.getById('builtin-translate')?.prompt).toBe('{{targetLanguage}}로 자연스럽게 번역') // 되돌리기 → 서버도 기본값 service.update('builtin-summarize', { prompt: getBuiltinInstructionDefaultPrompt('builtin-summarize') ?? '' }) enqueueChange('builtin_instructions', 'builtin-summarize', 'upsert') await engine.flush() expect(byKey('summarize')?.prompt).toBe(FakeSyncRemote.BUILTIN_DEFAULTS.summarize) }) it('다른 데스크톱에서 고친 프리셋 프롬프트를 받고, 기본값 복원도 반영한다', async () => { await remote.rpc('sync_set_builtin_instruction_prompt_v1', { p_builtin_key: 'formal', p_prompt: '회사 공문 말투로' }) const service = getCustomInstructionService() await engine.runFullSync() expect(service.getById('builtin-formal')?.prompt).toBe('회사 공문 말투로') await remote.rpc('sync_set_builtin_instruction_prompt_v1', { p_builtin_key: 'formal', p_prompt: null }) await engine.pull() expect(service.getById('builtin-formal')?.prompt).toBe(getBuiltinInstructionDefaultPrompt('builtin-formal')) // '자유 프롬프트'는 폰에 대응이 없어 건드리지 않는다 expect(service.getById('builtin-free-prompt')?.prompt).toBe('{{userPrompt}}') }) })