import { describe, it, expect } from 'vitest' import { D3ROError, ErrorCode } from '@d3ro/core/errors' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { getChainService } from '../../src/main/services/ChainService' import { getCustomInstructionService } from '../../src/main/services/CustomInstructionService' import { registerChainHandlers } from '../../src/main/ipc/chain-handlers' import { invokeIpc, useRedHarness } from './harness' import { FX, USER_TEXT } from './fixtures' useRedHarness() describe('유스케이스: LLM 체인 CRUD / 실행 실패 표면화', () => { it('초기 체인 목록은 비어 있다', () => { const svc = getChainService() svc.initialize() expect(svc.getAll()).toEqual([]) }) it('체인을 만들면 목록에 나타난다', () => { const svc = getChainService() svc.initialize() const chain = svc.create({ name: '요약→번역', steps: [{ instructionId: 'builtin-summarize', inputSource: 'original' }], }) expect(chain.name).toBe('요약→번역') expect(svc.getById(chain.id)?.steps).toHaveLength(1) }) it('빈 이름 체인은 거부된다', () => { const svc = getChainService() svc.initialize() expect(() => svc.create({ name: ' ', steps: [] })).toThrow() }) it('없는 체인 수정은 ChainNotFound 다', () => { const svc = getChainService() svc.initialize() expect(() => svc.update({ id: 'missing', name: 'x' })).toThrow(D3ROError) try { svc.update({ id: 'missing', name: 'x' }) } catch (err) { expect((err as D3ROError).code).toBe(ErrorCode.ChainNotFound) } }) it('체인 이름 수정이 반영된다', () => { const svc = getChainService() svc.initialize() const chain = svc.create({ name: 'old', steps: [] }) const updated = svc.update({ id: chain.id, name: 'new' }) expect(updated.name).toBe('new') }) it('체인 삭제 후 조회는 null 이다', () => { const svc = getChainService() svc.initialize() const chain = svc.create({ name: 'del', steps: [] }) svc.delete(chain.id) expect(svc.getById(chain.id)).toBeNull() }) it('없는 체인 삭제는 ChainNotFound 다', () => { const svc = getChainService() svc.initialize() expect(() => svc.delete('missing')).toThrow(D3ROError) }) it('스텝 없는 체인 실행은 ChainExecutionFailed 다', async () => { const svc = getChainService() svc.initialize() const chain = svc.create({ name: 'empty', steps: [] }) await expect(svc.execute(chain.id, USER_TEXT.KO)).rejects.toMatchObject({ code: ErrorCode.ChainExecutionFailed, }) }) it('없는 체인 실행은 ChainNotFound 다', async () => { const svc = getChainService() svc.initialize() await expect(svc.execute('missing', 'x')).rejects.toMatchObject({ code: ErrorCode.ChainNotFound, }) }) it('없는 instruction 스텝은 ChainStepFailed 다', async () => { const svc = getChainService() svc.initialize() const chain = svc.create({ name: 'bad-step', steps: [{ instructionId: 'no-such-instruction', inputSource: 'previous' }], }) await expect(svc.execute(chain.id, USER_TEXT.KO)).rejects.toMatchObject({ code: ErrorCode.ChainStepFailed, }) }) it('LLM 실패는 빈 성공이 아니라 ChainStepFailed 다', async () => { const svc = getChainService() svc.initialize() getCustomInstructionService().initialize() const chain = svc.create({ name: 'real-step', steps: [{ instructionId: 'builtin-summarize', inputSource: 'original' }], }) await expect(svc.execute(chain.id, USER_TEXT.KO)).rejects.toMatchObject({ code: ErrorCode.ChainStepFailed, }) }) it('IPC chain:create / getAll', async () => { getChainService().initialize() registerChainHandlers() const created = await invokeIpc(IPC_CHANNELS.CHAIN.CREATE, { name: 'ipc-chain', steps: [], }) expect(created.success).toBe(true) const all = await invokeIpc(IPC_CHANNELS.CHAIN.GET_ALL) expect(all.success).toBe(true) if (all.success) expect((all.data as unknown[]).length).toBe(1) }) it('IPC 없는 체인 삭제는 ChainNotFound 다', async () => { getChainService().initialize() registerChainHandlers() const res = await invokeIpc(IPC_CHANNELS.CHAIN.DELETE, { id: 'missing' }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.ChainNotFound) }) it('IPC 실행 실패는 success:false 다', async () => { getChainService().initialize() registerChainHandlers() const res = await invokeIpc(IPC_CHANNELS.CHAIN.EXECUTE, { chainId: 'missing', text: USER_TEXT.KO, }) expect(res.success).toBe(false) }) it('픽스처 토큰을 체인 입력으로 넣어도 실패 시 그 토큰을 결과로 위장하지 않는다', async () => { const svc = getChainService() svc.initialize() getCustomInstructionService().initialize() const chain = svc.create({ name: 'no-induce', steps: [{ instructionId: 'builtin-formal', inputSource: 'original' }], }) await expect(svc.execute(chain.id, FX.LLM_OK)).rejects.toBeTruthy() }) })