fix(sync): bind sync engine to its user DB, scope instructions/templates per account, harden navigation
This commit is contained in:
parent
1b8fe445f3
commit
9aa7302944
30 changed files with 2614 additions and 386 deletions
133
apps/desktop/tests/main/services/history-redteam-r1-0.test.ts
Normal file
133
apps/desktop/tests/main/services/history-redteam-r1-0.test.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// 이력 레드팀 r1-0: 삭제 시 녹음 WAV 정리, 대시보드 통계를 history 에서 계산
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createTestDb } from '../../helpers/createTestDb'
|
||||
import { bindTestDatabase, unbindTestDatabase } from '../../../src/main/db'
|
||||
import { history } from '../../../src/main/db/schema'
|
||||
import { initInMemoryConfig, resetInMemoryConfig } from '../../../src/main/services/ConfigService'
|
||||
import { getHistoryService, resetHistoryServiceForTests } from '../../../src/main/services/HistoryService'
|
||||
import { isAppRecordingPath, recordingsDir } from '../../../src/main/services/history-deletion'
|
||||
import { computeStreakDays, localDayKey } from '../../../src/main/services/history-stats'
|
||||
import { resetAccountScopeForTests } from '../../../src/main/services/account-scope'
|
||||
|
||||
let testDb: ReturnType<typeof createTestDb>
|
||||
const created: string[] = []
|
||||
|
||||
function writeRecording(name: string): string {
|
||||
const dir = recordingsDir()
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
const file = path.join(dir, name)
|
||||
fs.writeFileSync(file, Buffer.from('RIFF'))
|
||||
created.push(file)
|
||||
return file
|
||||
}
|
||||
|
||||
function insertHistory(opts: { audio?: string | null; createdAt?: number; status?: 'completed' | 'error'; words?: number; duration?: number }): string {
|
||||
const id = crypto.randomUUID()
|
||||
const at = opts.createdAt ?? Date.now()
|
||||
testDb.db
|
||||
.insert(history)
|
||||
.values({
|
||||
id,
|
||||
originalText: 'x',
|
||||
duration: opts.duration ?? 1,
|
||||
wordCount: opts.words ?? 1,
|
||||
status: opts.status ?? 'completed',
|
||||
audioLocalPath: opts.audio ?? null,
|
||||
createdAt: at,
|
||||
updatedAt: at,
|
||||
})
|
||||
.run()
|
||||
return id
|
||||
}
|
||||
|
||||
async function waitGone(file: string): Promise<void> {
|
||||
await vi.waitFor(() => expect(fs.existsSync(file)).toBe(false), { timeout: 2000, interval: 10 })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resetAccountScopeForTests()
|
||||
testDb = createTestDb()
|
||||
bindTestDatabase(testDb.db, '_local')
|
||||
initInMemoryConfig()
|
||||
resetHistoryServiceForTests()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
unbindTestDatabase()
|
||||
resetInMemoryConfig()
|
||||
testDb.close()
|
||||
for (const f of created.splice(0)) fs.rmSync(f, { force: true })
|
||||
resetAccountScopeForTests()
|
||||
})
|
||||
|
||||
describe('녹음 파일 정리', () => {
|
||||
it('단건 삭제는 녹음 WAV도 지운다', async () => {
|
||||
const wav = writeRecording(`${crypto.randomUUID()}.wav`)
|
||||
const id = insertHistory({ audio: wav })
|
||||
expect(getHistoryService().delete(id)).toBe(true)
|
||||
await waitGone(wav)
|
||||
})
|
||||
|
||||
it('전체 삭제·원격 삭제 반영도 녹음을 지우지만, 녹음 폴더 밖 사용자 원본은 건드리지 않는다', async () => {
|
||||
const wav1 = writeRecording(`${crypto.randomUUID()}.wav`)
|
||||
const wav2 = writeRecording(`${crypto.randomUUID()}.wav`)
|
||||
const outside = path.join(path.dirname(recordingsDir()), `user-source-${crypto.randomUUID()}.wav`)
|
||||
fs.writeFileSync(outside, Buffer.from('RIFF'))
|
||||
created.push(outside)
|
||||
insertHistory({ audio: wav1 })
|
||||
insertHistory({ audio: wav2 })
|
||||
insertHistory({ audio: outside })
|
||||
getHistoryService().deleteAll()
|
||||
await waitGone(wav1)
|
||||
await waitGone(wav2)
|
||||
expect(fs.existsSync(outside)).toBe(true)
|
||||
expect(testDb.db.select().from(history).all()).toEqual([])
|
||||
})
|
||||
|
||||
it('동기화의 원격 삭제 반영(historyAdapter.deleteLocal)도 녹음을 지운다', async () => {
|
||||
const { adapterFor } = await import('../../../src/main/services/sync/sync-adapters')
|
||||
const wav = writeRecording(`${crypto.randomUUID()}.wav`)
|
||||
const id = insertHistory({ audio: wav })
|
||||
expect(adapterFor('history')?.deleteLocal(id)).toBe(true)
|
||||
await waitGone(wav)
|
||||
})
|
||||
|
||||
it('녹음 폴더 경계 판정', () => {
|
||||
const dir = recordingsDir()
|
||||
expect(isAppRecordingPath(path.join(dir, 'a.wav'), dir)).toBe(true)
|
||||
expect(isAppRecordingPath(path.join(dir, '..', 'a.wav'), dir)).toBe(false)
|
||||
expect(isAppRecordingPath(`${dir}-evil${path.sep}a.wav`, dir)).toBe(false)
|
||||
expect(isAppRecordingPath(dir, dir)).toBe(false)
|
||||
expect(isAppRecordingPath(null, dir)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('대시보드 통계', () => {
|
||||
it('동기화·가져오기로 들어온 기록도 누적 통계에 들어가고 연속 사용일을 계산한다', () => {
|
||||
const day = 24 * 60 * 60 * 1000
|
||||
const now = Date.now()
|
||||
// create()를 거치지 않은 행(동기화 반영) — 예전 카운터에는 반영되지 않았다
|
||||
insertHistory({ createdAt: now, words: 5, duration: 2 })
|
||||
insertHistory({ createdAt: now - day, words: 3, duration: 1 })
|
||||
insertHistory({ createdAt: now - 2 * day, words: 2, duration: 1 })
|
||||
insertHistory({ createdAt: now - 2 * day, status: 'error', words: 100, duration: 50 })
|
||||
const stats = getHistoryService().getStats()
|
||||
expect(stats.totalSessionCount).toBe(3)
|
||||
expect(stats.totalWordCount).toBe(10)
|
||||
expect(stats.totalRecordingTimeMs).toBe(4000)
|
||||
expect(stats.streakDays).toBe(3)
|
||||
})
|
||||
|
||||
it('computeStreakDays: 오늘 기록이 없으면 어제부터, 끊기면 멈춘다', () => {
|
||||
const today = new Date(2026, 8, 28, 15)
|
||||
const key = (offset: number): string => localDayKey(new Date(2026, 8, 28 - offset))
|
||||
expect(computeStreakDays([], today)).toBe(0)
|
||||
expect(computeStreakDays([key(0), key(1), key(2)], today)).toBe(3)
|
||||
expect(computeStreakDays([key(1), key(2)], today)).toBe(2)
|
||||
expect(computeStreakDays([key(0), key(2)], today)).toBe(1)
|
||||
expect(computeStreakDays([key(2), key(3)], today)).toBe(0)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue