import { describe, it, expect } from 'vitest' import { ErrorCode } from '@d3ro/core/errors' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getCustomInstructionService } from '../../src/main/services/CustomInstructionService' import { registerInstructionHandlers } from '../../src/main/ipc/instruction-handlers' import { invokeIpc, useRedHarness } from './harness' import { USER_TEXT } from './fixtures' useRedHarness() describe('유스케이스: 커스텀 명령어 CRUD / 프리셋 보호 / IPC', () => { it('첫 실행 시 빌트인 5개가 있다', () => { const svc = getCustomInstructionService() svc.initialize() expect(svc.getAll()).toHaveLength(5) expect(svc.getAll().every((i) => i.isBuiltin)).toBe(true) }) it('빌트인 번역 명령어를 id 로 연다', () => { const svc = getCustomInstructionService() svc.initialize() expect(svc.getById('builtin-translate')?.id).toBe('builtin-translate') }) it('없는 명령어 조회는 null 이다', () => { const svc = getCustomInstructionService() svc.initialize() expect(svc.getById('nope')).toBeNull() }) it('사용자 명령어를 추가하면 목록이 6개가 된다', () => { const svc = getCustomInstructionService() svc.initialize() const created = svc.create({ name: '내 명령', description: '설명', prompt: '다음을 고쳐라: {{text}}', icon: 'Edit', }) expect(created.isBuiltin).toBe(false) expect(svc.getAll()).toHaveLength(6) }) it('빈 이름 명령어 추가는 거부된다', () => { const svc = getCustomInstructionService() svc.initialize() expect(() => svc.create({ name: ' ', description: 'd', prompt: 'p', icon: 'Edit' }), ).toThrow() }) it('빈 프롬프트 추가는 거부된다', () => { const svc = getCustomInstructionService() svc.initialize() expect(() => svc.create({ name: 'n', description: 'd', prompt: '', icon: 'Edit' }), ).toThrow() }) it('사용자 명령어 이름을 수정한다', () => { const svc = getCustomInstructionService() svc.initialize() const created = svc.create({ name: 'old', description: 'd', prompt: 'p', icon: 'Edit' }) const updated = svc.update(created.id, { name: 'new' }) expect(updated?.name).toBe('new') }) it('빌트인은 이름 수정이 적용되지 않고 프롬프트만 바뀐다', () => { const svc = getCustomInstructionService() svc.initialize() const updated = svc.update('builtin-summarize', { name: '해킹', prompt: '새 프롬프트' }) expect(updated?.name).not.toBe('해킹') expect(updated?.prompt).toBe('새 프롬프트') }) it('없는 명령어 수정은 null 이다', () => { const svc = getCustomInstructionService() svc.initialize() expect(svc.update('missing', { name: 'x' })).toBeNull() }) it('사용자 명령어는 삭제된다', () => { const svc = getCustomInstructionService() svc.initialize() const created = svc.create({ name: 'del', description: 'd', prompt: 'p', icon: 'Edit' }) expect(svc.delete(created.id)).toBe(true) expect(svc.getById(created.id)).toBeNull() }) it('빌트인 삭제는 거부된다', () => { const svc = getCustomInstructionService() svc.initialize() expect(svc.delete('builtin-translate')).toBe(false) expect(svc.getById('builtin-translate')).not.toBeNull() }) it('없는 명령어 삭제는 false 이다', () => { const svc = getCustomInstructionService() svc.initialize() expect(svc.delete('missing')).toBe(false) }) it('순서를 바꾸면 getAll 순서가 바뀐다', () => { const svc = getCustomInstructionService() svc.initialize() const ids = svc.getAll().map((i) => i.id).reverse() svc.reorder(ids) expect(svc.getAll().map((i) => i.id)).toEqual(ids) }) it('resetBuiltins 후 빌트인이 다시 5개다', () => { const svc = getCustomInstructionService() svc.initialize() svc.update('builtin-translate', { prompt: 'changed' }) svc.create({ name: 'keep', description: 'd', prompt: 'p', icon: 'Edit' }) svc.resetBuiltins() expect(svc.getById('builtin-translate')?.prompt).not.toBe('changed') expect(svc.getAll().filter((i) => !i.isBuiltin)).toHaveLength(1) }) it('유니코드 이름/프롬프트가 저장된다', () => { const svc = getCustomInstructionService() svc.initialize() const created = svc.create({ name: USER_TEXT.UNICODE, description: USER_TEXT.UNICODE, prompt: USER_TEXT.UNICODE, icon: 'Edit', }) expect(svc.getById(created.id)?.name).toBe(USER_TEXT.UNICODE) }) it('IPC instruction:getAll 은 초기화 후 5개 이상이다', async () => { getCustomInstructionService().initialize() registerInstructionHandlers() const res = await invokeIpc(IPC_CHANNELS.INSTRUCTION.GET_ALL) expect(res.success).toBe(true) if (res.success) expect((res.data as unknown[]).length).toBeGreaterThanOrEqual(5) }) it('IPC instruction:create 왕복', async () => { getCustomInstructionService().initialize() registerInstructionHandlers() const res = await invokeIpc(IPC_CHANNELS.INSTRUCTION.CREATE, { name: 'ipc', description: 'd', prompt: 'p', }) expect(res.success).toBe(true) if (res.success) expect(res.data.name).toBe('ipc') }) it('IPC instruction:delete 빌트인은 실패한다', async () => { getCustomInstructionService().initialize() registerInstructionHandlers() const res = await invokeIpc(IPC_CHANNELS.INSTRUCTION.DELETE, { id: 'builtin-formal' }) expect(res.success).toBe(false) }) it('IPC instruction:delete 없는 id 는 빌트인 삭제와 다른 코드다', async () => { getCustomInstructionService().initialize() registerInstructionHandlers() const missing = await invokeIpc(IPC_CHANNELS.INSTRUCTION.DELETE, { id: 'missing' }) const builtin = await invokeIpc(IPC_CHANNELS.INSTRUCTION.DELETE, { id: 'builtin-explain-code' }) expect(missing.success).toBe(false) expect(builtin.success).toBe(false) if (!missing.success && !builtin.success) { expect(missing.error.code).not.toBe(builtin.error.code) } }) it('IPC instruction:getById 없는 id 는 null 이다', async () => { getCustomInstructionService().initialize() registerInstructionHandlers() const res = await invokeIpc(IPC_CHANNELS.INSTRUCTION.GET_BY_ID, { id: 'missing' }) expect(res.success).toBe(true) if (res.success) expect(res.data).toBeNull() }) })