import { describe, it, expect } from 'vitest' import { D3ROError, ErrorCode } from '@d3ro/core/errors' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getDictionaryService } from '../../src/main/services/DictionaryService' import { registerDictionaryHandlers } from '../../src/main/ipc/dictionary-handlers' import { invokeIpc, useRedHarness } from './harness' import { USER_TEXT } from './fixtures' useRedHarness() describe('유스케이스: 사용자 사전 CRUD / 검색 / 힌트 / IPC', () => { it('빈 사전 목록은 0건이다', () => { const page = getDictionaryService().list({ page: 0, pageSize: 20 }) expect(page.total).toBe(0) expect(page.entries).toEqual([]) }) it('단어를 추가하면 목록에 나타난다', () => { const entry = getDictionaryService().add({ word: 'D3RO' }) expect(entry.word).toBe('D3RO') expect(entry.category).toBe('user') expect(entry.usageCount).toBe(0) expect(getDictionaryService().list({ page: 0, pageSize: 10 }).total).toBe(1) }) it('발음과 기술 카테고리를 지정해 추가한다', () => { const entry = getDictionaryService().add({ word: 'Whisper', pronunciation: '위스퍼', category: 'technical', }) expect(entry.pronunciation).toBe('위스퍼') expect(entry.category).toBe('technical') }) it('유니코드 단어도 추가된다', () => { const entry = getDictionaryService().add({ word: USER_TEXT.UNICODE }) expect(entry.word).toBe(USER_TEXT.UNICODE) }) it('빈 단어 추가는 거부된다', () => { expect(() => getDictionaryService().add({ word: ' ' })).toThrow(D3ROError) try { getDictionaryService().add({ word: '' }) expect.unreachable() } catch (err) { expect(err).toBeInstanceOf(D3ROError) expect((err as D3ROError).code).toBe(ErrorCode.DictionaryImportInvalidFormat) } }) it('같은 단어+카테고리 중복 추가는 DictionaryDuplicate 이다', () => { getDictionaryService().add({ word: USER_TEXT.DUP }) expect(() => getDictionaryService().add({ word: USER_TEXT.DUP })).toThrow(D3ROError) try { getDictionaryService().add({ word: USER_TEXT.DUP }) } catch (err) { expect((err as D3ROError).code).toBe(ErrorCode.DictionaryDuplicate) } }) it('같은 단어를 다른 카테고리로 추가할 수 있다', () => { getDictionaryService().add({ word: 'API', category: 'user' }) const tech = getDictionaryService().add({ word: 'API', category: 'technical' }) expect(tech.category).toBe('technical') expect(getDictionaryService().list({ page: 0, pageSize: 10 }).total).toBe(2) }) it('단어 수정이 반영된다', () => { const created = getDictionaryService().add({ word: 'old' }) const updated = getDictionaryService().update({ id: created.id, word: 'new' }) expect(updated?.word).toBe('new') expect(getDictionaryService().list({ page: 0, pageSize: 10 }).entries[0].word).toBe('new') }) it('없는 id 수정은 null 이다', () => { expect(getDictionaryService().update({ id: 'missing', word: 'x' })).toBeNull() }) it('단어 삭제 후 목록에서 사라진다', () => { const created = getDictionaryService().add({ word: 'gone' }) expect(getDictionaryService().delete(created.id)).toBe(true) expect(getDictionaryService().list({ page: 0, pageSize: 10 }).total).toBe(0) }) it('없는 id 삭제는 false 이다', () => { expect(getDictionaryService().delete('missing')).toBe(false) }) it('검색은 부분 일치한다', () => { getDictionaryService().add({ word: '인공지능' }) getDictionaryService().add({ word: '자연어' }) const found = getDictionaryService().search({ query: '인공', page: 0, pageSize: 10 }) expect(found.total).toBe(1) expect(found.entries[0].word).toBe('인공지능') }) it('검색 무결과는 빈 페이지이다', () => { getDictionaryService().add({ word: 'foo' }) expect(getDictionaryService().search({ query: 'zzz', page: 0, pageSize: 10 }).total).toBe(0) }) it('사용 횟수 증가가 저장된다', () => { const created = getDictionaryService().add({ word: 'hint' }) getDictionaryService().incrementUsage(created.id) getDictionaryService().incrementUsage(created.id) const listed = getDictionaryService().list({ page: 0, pageSize: 10 }).entries[0] expect(listed.usageCount).toBe(2) expect(listed.lastUsedAt).toBeGreaterThan(0) }) it('getPromptHints 는 사용 빈도 순 단어 목록이다', () => { const a = getDictionaryService().add({ word: 'alpha' }) getDictionaryService().add({ word: 'beta' }) getDictionaryService().incrementUsage(a.id) const hints = getDictionaryService().getPromptHints() expect(hints.startsWith('alpha')).toBe(true) expect(hints).toContain('beta') }) it('빈 사전의 prompt hint 는 빈 문자열이다', () => { expect(getDictionaryService().getPromptHints()).toBe('') }) it('IPC dictionary:add / getAll 왕복', async () => { registerDictionaryHandlers() const add = await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: 'IPCWORD' }) expect(add.success).toBe(true) const all = await invokeIpc(IPC_CHANNELS.DICTIONARY.GET_ALL, { page: 0, pageSize: 10 }) expect(all.success).toBe(true) if (all.success) expect(all.data.total).toBe(1) }) it('IPC dictionary:add 빈 단어는 실패한다', async () => { registerDictionaryHandlers() const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: ' ' }) expect(res.success).toBe(false) }) it('IPC dictionary:add 중복은 DictionaryDuplicate 이다', async () => { registerDictionaryHandlers() await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: 'dup' }) const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.ADD, { word: 'dup' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryDuplicate) }) it('IPC dictionary:update 없는 id 는 DictionaryNotFound 이다', async () => { registerDictionaryHandlers() const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.UPDATE, { id: 'missing', word: 'x' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryNotFound) }) it('IPC dictionary:delete 없는 id 는 DictionaryNotFound 이다', async () => { registerDictionaryHandlers() const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.DELETE, { id: 'missing' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.DictionaryNotFound) }) it('IPC dictionary:search 는 서비스 검색을 노출한다', async () => { registerDictionaryHandlers() getDictionaryService().add({ word: '검색대상' }) const res = await invokeIpc(IPC_CHANNELS.DICTIONARY.SEARCH, { query: '검색', page: 0, pageSize: 10 }) expect(res.success).toBe(true) if (res.success) expect(res.data.total).toBe(1) }) })