d3ro-voice/apps/desktop/tests/main/services/dictionary-file-codec.test.ts
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
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.
2026-09-28 20:45:52 +09:00

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 }
})
})
})