// tests/main/sync/audio-sync-redteam-r3-1.test.ts // 동기화 회귀 (r3-1): // - 파일 전사 기록(audioLocalPath = 사용자가 고른 원본 미디어)은 '녹음 동기화'로 올리지 않는다 // - 대기 중인 원격 삭제를 뒤늦은 upsert(예: 자동 제목)가 덮어쓰지 않는다 import fs from 'fs' import os from 'os' import path from 'path' 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 { history } from '../../../src/main/db/schema' import { initInMemoryConfig, resetInMemoryConfig } from '../../../src/main/services/ConfigService' import { listLocalAudioOwners, localAudioPath } from '../../../src/main/services/sync/audio-sync' import { SyncEngine } from '../../../src/main/services/sync/SyncEngine' import { enqueueChange, pendingOps } from '../../../src/main/services/sync/sync-outbox' const USER = '11111111-1111-4111-8111-111111111111' let testDb: ReturnType let remote: FakeSyncRemote let engine: SyncEngine let tmpDir: string beforeEach(() => { testDb = createTestDb() bindTestDatabase(testDb.db, USER) initInMemoryConfig() vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('fetch failed')) remote = new FakeSyncRemote(USER) engine = new SyncEngine({ remote, userId: USER }) tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'd3ro-audio-r3-1-')) }) afterEach(() => { vi.restoreAllMocks() engine.dispose() unbindTestDatabase() resetInMemoryConfig() testDb.close() fs.rmSync(tmpDir, { recursive: true, force: true }) }) function insertHistory(mode: 'dictation' | 'file-transcription', fileName: string): string { const id = crypto.randomUUID() const file = path.join(tmpDir, fileName) fs.writeFileSync(file, Buffer.from('RIFF....WAVEfmt fake media')) const now = Date.now() testDb.db .insert(history) .values({ id, originalText: 'spoken', duration: 2, mode, audioLocalPath: file, createdAt: now, updatedAt: now }) .run() return id } describe('파일 전사 원본은 녹음으로 올리지 않는다 (item 19)', () => { it('파일 전사 기록의 원본 미디어는 업로드 대상이 아니다', async () => { const fileId = insertHistory('file-transcription', 'meeting-video.mp4') const dictationId = insertHistory('dictation', 'dictation.wav') expect(localAudioPath('history', fileId)).toBeNull() expect(localAudioPath('history', dictationId)).not.toBeNull() expect(listLocalAudioOwners('history')).toEqual([dictationId]) const result = await engine.runFullSync() expect(result.errors).toEqual([]) expect(remote.objects.has(`audio/${USER}/desktop/history/${fileId}.mp4`)).toBe(false) expect(remote.objects.has(`audio/${USER}/desktop/history/${dictationId}.wav`)).toBe(true) }) }) describe('대기 중인 삭제는 뒤늦은 upsert 에 덮이지 않는다 (item 37)', () => { it('delete 뒤에 같은 행의 upsert 가 와도 op 는 delete 로 남는다', () => { const id = crypto.randomUUID() enqueueChange('history', id, 'upsert') enqueueChange('history', id, 'delete') enqueueChange('history', id, 'upsert') expect(pendingOps('history').get(id)).toBe('delete') }) })