import { describe, it, expect } from 'vitest' import fs from 'fs' import os from 'os' import path from 'path' import { ErrorCode } from '@d3ro/core/errors' import { getRAGService } from '../../src/main/services/RAGService' import { getFileTranscriptionService } from '../../src/main/services/FileTranscriptionService' import { useRedHarness } from './harness' import { USER_TEXT } from './fixtures' useRedHarness() function writeTemp(name: string, body: string): string { const p = path.join(os.tmpdir(), `d3ro-red-${Date.now()}-${name}`) fs.writeFileSync(p, body, 'utf-8') return p } describe('유스케이스: 지식베이스 문서 추가/삭제/질의 실패', () => { it('초기 문서 목록은 비어 있다', () => { expect(getRAGService().getDocuments()).toEqual([]) expect(getRAGService().getStateInfo().documentCount).toBe(0) }) it('지원하지 않는 확장자는 RAGUnsupportedFormat 이다', async () => { const p = writeTemp('x.bin', 'aaaa') await expect(getRAGService().addDocument(p)).rejects.toMatchObject({ code: ErrorCode.RAGUnsupportedFormat, }) }) it('너무 짧은 텍스트는 RAGIndexingFailed 다', async () => { const p = writeTemp('short.txt', 'hi') await expect(getRAGService().addDocument(p)).rejects.toMatchObject({ code: ErrorCode.RAGIndexingFailed, }) }) it('충분한 txt 문서를 추가하면 목록에 나타난다', async () => { const p = writeTemp('doc.txt', `${USER_TEXT.KO}\n`.repeat(5)) const doc = await getRAGService().addDocument(p) expect(doc.fileType).toBe('txt') expect(getRAGService().getDocuments().some((d) => d.id === doc.id)).toBe(true) }) it('문서를 제거하면 목록에서 사라진다', async () => { const p = writeTemp('rm.txt', `${USER_TEXT.EN}\n`.repeat(8)) const doc = await getRAGService().addDocument(p) getRAGService().removeDocument(doc.id) expect(getRAGService().getDocuments().some((d) => d.id === doc.id)).toBe(false) }) it('없는 문서 재인덱스는 RAGDocumentNotFound 다', async () => { await expect(getRAGService().reindex('missing')).rejects.toMatchObject({ code: ErrorCode.RAGDocumentNotFound, }) }) it('청크가 없을 때 query 는 빈 답을 성공으로 위장하지 않는다', async () => { await expect(getRAGService().query('무엇이든')).rejects.toMatchObject({ code: ErrorCode.RAGQueryFailed, }) }) it('임베딩 서버가 없으면 인덱싱이 indexed=true + 0 chunks 로 성공 위장하지 않는다', async () => { const p = writeTemp('emb.txt', `${USER_TEXT.KO}\n`.repeat(6)) const doc = await getRAGService().addDocument(p) await new Promise((r) => setTimeout(r, 80)) const stored = getRAGService().getDocuments().find((d) => d.id === doc.id) if (stored?.indexed) { expect(stored.chunkCount).toBeGreaterThan(0) } else { expect(stored?.indexed).toBe(false) } }) }) describe('유스케이스: 파일 전사 입력 검증 / 취소', () => { it('초기 상태는 idle 이다', () => { expect(getFileTranscriptionService().state).toBe('idle') expect(getFileTranscriptionService().getStateInfo().jobId).toBeNull() }) it('지원하지 않는 포맷은 FileTranscriptionInvalidFormat 이다', async () => { const p = writeTemp('x.txt', 'not audio') await expect(getFileTranscriptionService().startTranscription(p)).rejects.toMatchObject({ code: ErrorCode.FileTranscriptionInvalidFormat, }) }) it('없는 파일은 숨기지 않고 던진다', async () => { await expect( getFileTranscriptionService().startTranscription(path.join(os.tmpdir(), 'nope.wav')), ).rejects.toBeTruthy() }) it('취소는 idle 이 아니어도 안전하다', () => { expect(() => getFileTranscriptionService().cancel()).not.toThrow() }) })