114 lines
5.4 KiB
TypeScript
114 lines
5.4 KiB
TypeScript
// tests/red/data-policy-redteam-r2-1.test.ts
|
|
// 로컬이 서버 쓰기 제약과 같은 규칙으로 입력을 거절한다(실제 서비스 + in-memory DB):
|
|
// - 사전: 대소문자만 다른 단어는 같은 단어(서버 lower(btrim(word)) 유일성) — 추가·수정·가져오기
|
|
// - 명령어: 편집으로 이름·프롬프트를 비울 수 없다(생성과 같은 규칙)
|
|
// - 메모 태그: 80자를 넘으면 저장하지 않는다
|
|
// - 검색: 기록은 다듬은 문장·제목·요약으로도 찾고, %·_ 는 글자 그대로 찾는다(사전 검색 포함)
|
|
|
|
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 { useRedHarness, historyInput } from './harness'
|
|
|
|
useRedHarness()
|
|
|
|
import { getDictionaryService } from '../../src/main/services/DictionaryService'
|
|
import { getCustomInstructionService } from '../../src/main/services/CustomInstructionService'
|
|
import { getMemoService } from '../../src/main/services/MemoService'
|
|
import { getHistoryService } from '../../src/main/services/HistoryService'
|
|
|
|
describe('사전 — 대소문자 무시 식별', () => {
|
|
it("'API' 가 있으면 'api' 추가는 DictionaryDuplicate 다(발음이 동기화에서 조용히 사라지지 않게)", () => {
|
|
const dict = getDictionaryService()
|
|
dict.add({ word: 'API' })
|
|
expect(() => dict.add({ word: ' api ', pronunciation: '에이피아이' })).toThrow(
|
|
expect.objectContaining({ code: ErrorCode.DictionaryDuplicate }),
|
|
)
|
|
})
|
|
|
|
it('다른 카테고리는 같은 단어를 허용한다', () => {
|
|
const dict = getDictionaryService()
|
|
dict.add({ word: 'API', category: 'user' })
|
|
expect(() => dict.add({ word: 'api', category: 'technical' })).not.toThrow()
|
|
})
|
|
|
|
it('수정으로 다른 항목과 대소문자만 다른 단어가 되면 거절하고, 자기 자신의 대소문자 변경은 허용한다', () => {
|
|
const dict = getDictionaryService()
|
|
dict.add({ word: 'Kubernetes' })
|
|
const other = dict.add({ word: 'Docker' })
|
|
expect(() => dict.update({ id: other.id, word: 'KUBERNETES' })).toThrow(
|
|
expect.objectContaining({ code: ErrorCode.DictionaryDuplicate }),
|
|
)
|
|
expect(dict.update({ id: other.id, word: 'DOCKER' })?.word).toBe('DOCKER')
|
|
})
|
|
|
|
it('가져오기는 대소문자만 다른 단어를 건너뛴다', () => {
|
|
const dict = getDictionaryService()
|
|
dict.add({ word: 'GraphQL' })
|
|
const file = path.join(os.tmpdir(), `d3ro-dict-r2-1-${Date.now()}.json`)
|
|
fs.writeFileSync(file, JSON.stringify([{ word: 'graphql' }, { word: 'gRPC' }, { word: 'GRPC' }]))
|
|
try {
|
|
const result = dict.importDictionary({ filePath: file, format: 'json' })
|
|
expect(result).toEqual({ imported: 1, skipped: 2, errors: 0 })
|
|
} finally {
|
|
fs.rmSync(file, { force: true })
|
|
}
|
|
})
|
|
|
|
it('사전 검색의 %·_ 는 와일드카드가 아니다', () => {
|
|
const dict = getDictionaryService()
|
|
dict.add({ word: 'alpha' })
|
|
dict.add({ word: 'snake_case' })
|
|
expect(dict.search({ query: '_', page: 0, pageSize: 20 }).entries.map((e) => e.word)).toEqual(['snake_case'])
|
|
expect(dict.search({ query: '%', page: 0, pageSize: 20 }).total).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('명령어 — 빈 값 검증', () => {
|
|
it('편집으로 프롬프트나 이름을 비울 수 없다', () => {
|
|
const svc = getCustomInstructionService()
|
|
svc.initialize()
|
|
const created = svc.create({ name: '요약해', description: '', prompt: '요약해 주세요' })
|
|
expect(() => svc.update(created.id, { prompt: ' ' })).toThrow(/prompt is empty/)
|
|
expect(() => svc.update(created.id, { name: '' })).toThrow(/name is empty/)
|
|
expect(svc.getById(created.id)?.prompt).toBe('요약해 주세요')
|
|
})
|
|
})
|
|
|
|
describe('메모 태그 — 서버 길이 제한', () => {
|
|
it('80자를 넘는 태그는 저장하지 않는다(80자는 허용)', () => {
|
|
const entry = getHistoryService().create(historyInput())
|
|
const memo = getMemoService()
|
|
expect(() => memo.addTag(entry.id, 't'.repeat(81))).toThrow(/too long/)
|
|
expect(memo.getTagsForEntry(entry.id)).toEqual([])
|
|
expect(memo.addTag(entry.id, 't'.repeat(80)).tag).toHaveLength(80)
|
|
})
|
|
})
|
|
|
|
describe('기록 검색', () => {
|
|
it('카드에 보이는 다듬은 문장·제목·요약으로도 찾는다', () => {
|
|
const history = getHistoryService()
|
|
history.create(historyInput({ originalText: '한국어 원문', polishedText: 'Quarterly roadmap review' }))
|
|
history.create(historyInput({ originalText: '다른 원문', title: '출시 회의록' }))
|
|
history.create(historyInput({ originalText: '세 번째', summaryText: '결정: 예산 증액' }))
|
|
|
|
const search = (query: string) => history.search({ query, page: 0, pageSize: 20 }).entries.length
|
|
expect(search('roadmap')).toBe(1)
|
|
expect(search('출시 회의')).toBe(1)
|
|
expect(search('예산 증액')).toBe(1)
|
|
})
|
|
|
|
it('%·_ 는 글자 그대로 찾는다', () => {
|
|
const history = getHistoryService()
|
|
history.create(historyInput({ originalText: '매출이 100% 늘었다' }))
|
|
history.create(historyInput({ originalText: '매출이 1005 건이다' }))
|
|
history.create(historyInput({ originalText: 'file_name 확인' }))
|
|
|
|
const texts = (query: string) =>
|
|
history.search({ query, page: 0, pageSize: 20 }).entries.map((e) => e.originalText)
|
|
expect(texts('100%')).toEqual(['매출이 100% 늘었다'])
|
|
expect(texts('_')).toEqual(['file_name 확인'])
|
|
})
|
|
})
|