Batch of red-team r3 fixes that were in the working tree before the 2026-09-28 design overhaul, committed as one unit with their tests. - desktop main: STT timeouts and sidecar, voice recording store, sync (credentials, audio, knowledge reindex, push gates), runtime provisioner, update policy, AltGr keybindings, voice-command policy, dictionary file codec/limits, meeting transcript condensing and a local recording ledger so interrupted-session recovery only closes meetings this device recorded (a phone's live meeting is left alone). - mobile: login CSRF via implicit token callbacks rejected, account deletion/retention, durable queue retention, knowledge realtime without unfiltered DELETE, meeting re-record failure paths, cloud STT client, preferences store/resync. - core: text chunking splits long unbroken transcripts to fit, template field policy, dictionary limits, meeting markdown inline handling. - server: payple webhook policy and cancellation order scope, meeting document generation quota, team RPC null-role guard, unified LLM quota in-flight accounting, knowledge chunk vector index, meeting re-record failure paths (migrations 20260929*). - ci: portable/runtime feed gates, update-policy schema, Forgejo file delete and alias planning. Four older tests are updated to the new contracts rather than the old behavior: token-pair auth callbacks are rejected, knowledge realtime no longer subscribes to DELETE, long transcript lines are split, and meeting recovery requires the local recording ledger for empty rows.
89 lines
3 KiB
TypeScript
89 lines
3 KiB
TypeScript
// tests/main/services/dictionary-file-codec.test.ts
|
|
// 사전 파일 형식(CSV·JSON) 순수 코덱 — DictionaryService 에서 분리한 로직의 단위 테스트
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
DICTIONARY_CSV_HEADER,
|
|
parseDictionaryFile,
|
|
serializeDictionary,
|
|
toDictionaryImportRow
|
|
} from '../../../src/main/services/dictionary/dictionary-file-codec'
|
|
import type { DictionaryEntry } from '@d3ro/core/types'
|
|
|
|
const entry: DictionaryEntry = {
|
|
id: 'e1',
|
|
word: '=SUM, "quoted"',
|
|
pronunciation: null,
|
|
category: 'technical',
|
|
usageCount: 3,
|
|
lastUsedAt: null,
|
|
createdAt: 100,
|
|
updatedAt: 200
|
|
}
|
|
|
|
describe('serializeDictionary', () => {
|
|
it('CSV 는 BOM·헤더·CRLF 를 쓰고 수식·따옴표를 이스케이프한다', () => {
|
|
const csv = serializeDictionary([entry], 'csv')
|
|
expect(csv.startsWith(`\uFEFF${DICTIONARY_CSV_HEADER.join(',')}\r\n`)).toBe(true)
|
|
expect(csv).toContain('"=SUM, ""quoted""",,technical,3,100,200\r\n')
|
|
})
|
|
|
|
it('JSON 은 entries 객체로 쓴다', () => {
|
|
expect(JSON.parse(serializeDictionary([entry], 'json'))).toEqual({ entries: [entry] })
|
|
})
|
|
|
|
it('CSV 로 쓴 내용을 다시 읽으면 같은 값이 나온다', () => {
|
|
const [record] = parseDictionaryFile(serializeDictionary([entry], 'csv'), 'csv')
|
|
expect(record).toMatchObject({ word: entry.word, category: 'technical', usageCount: '3' })
|
|
})
|
|
})
|
|
|
|
describe('parseDictionaryFile', () => {
|
|
it('JSON 배열과 entries 객체를 모두 받는다', () => {
|
|
expect(parseDictionaryFile('[{"word":"a"}, 1, null]', 'json')).toEqual([{ word: 'a' }])
|
|
expect(parseDictionaryFile('{"entries":[{"word":"b"}]}', 'json')).toEqual([{ word: 'b' }])
|
|
})
|
|
|
|
it('형식이 맞지 않으면 Error 를 던진다', () => {
|
|
expect(() => parseDictionaryFile('{"x":1}', 'json')).toThrow(/entries/)
|
|
expect(() => parseDictionaryFile('', 'csv')).toThrow(/empty CSV/)
|
|
expect(() => parseDictionaryFile('term\r\nx\r\n', 'csv')).toThrow(/word/)
|
|
})
|
|
})
|
|
|
|
describe('toDictionaryImportRow', () => {
|
|
it('필드를 정규화하고 기본값을 채운다', () => {
|
|
expect(
|
|
toDictionaryImportRow(
|
|
{ word: ' AI ', category: 'bogus', usage_count: '2.7', created_at: 50 },
|
|
999
|
|
)
|
|
).toEqual({
|
|
ok: true,
|
|
row: {
|
|
word: 'AI',
|
|
pronunciation: null,
|
|
category: 'user',
|
|
usageCount: 2,
|
|
lastUsedAt: null,
|
|
createdAt: 50,
|
|
updatedAt: 50
|
|
}
|
|
})
|
|
})
|
|
|
|
it('빈 단어·너무 긴 값을 문제로 돌려준다', () => {
|
|
expect(toDictionaryImportRow({ pronunciation: 'x' }, 1)).toMatchObject({
|
|
ok: false,
|
|
problem: { reason: 'empty-word' }
|
|
})
|
|
expect(toDictionaryImportRow({ word: 'w'.repeat(121) }, 1)).toMatchObject({
|
|
ok: false,
|
|
problem: { reason: 'too-long', field: 'word', max: 120 }
|
|
})
|
|
expect(toDictionaryImportRow({ word: 'ok', pronunciation: 'p'.repeat(201) }, 1)).toMatchObject({
|
|
ok: false,
|
|
problem: { reason: 'too-long', field: 'pronunciation', max: 200 }
|
|
})
|
|
})
|
|
})
|