/** * Requirement Graph (Self-Evolving OS Phase 1 / Track 2-1) 단위 테스트. * 순수 함수만 검증 — vscode 의존 없음. */ import { DEFAULT_TASK_REQUIREMENTS, detectTaskType, buildRequirementGraphBlock, checkRequirementCoverage, formatRequirementCoverageFooter, } from '../src/intelligence/requirementGraph'; describe('detectTaskType', () => { it('회의록 요청을 감지한다', () => { expect(detectTaskType('오늘 주간회의 내용 회의록으로 정리해줘')?.id).toBe('meeting-minutes'); expect(detectTaskType('어제 미팅 노트 만들어줘')?.id).toBe('meeting-minutes'); }); it('시장조사 요청을 감지한다', () => { expect(detectTaskType('전기차 충전 인프라 시장조사 해줘')?.id).toBe('market-research'); expect(detectTaskType('국내 로봇청소기 시장 규모 분석 부탁해')?.id).toBe('market-research'); }); it('일정 관리 요청을 감지한다', () => { expect(detectTaskType('내일 3시에 미팅 잡아줘')?.id).toBe('schedule'); expect(detectTaskType('이번 주 일정 확인해줘')?.id).toBe('schedule'); }); it('범용 조사 요청은 업무조사로 감지한다 (시장조사보다 후순위)', () => { expect(detectTaskType('MCP 프로토콜에 대해 조사해줘')?.id).toBe('work-research'); }); it('일반 잡담·빈 입력은 null', () => { expect(detectTaskType('안녕! 오늘 기분 어때?')).toBeNull(); expect(detectTaskType('')).toBeNull(); expect(detectTaskType(' ')).toBeNull(); }); }); describe('buildRequirementGraphBlock', () => { it('회의록 블록에 필수 요소 5종이 모두 포함된다', () => { const block = buildRequirementGraphBlock('회의록 정리해줘'); expect(block).toContain('[TASK REQUIREMENTS — 회의록]'); for (const label of ['참석자', '결정사항', '액션 아이템', '담당자', '기한']) { expect(block).toContain(label); } expect(block).toContain('(확인 필요)'); // 조용한 생략 금지 지시 expect(block).toContain('[/TASK REQUIREMENTS]'); }); it('업무 유형 미감지 시 빈 문자열 (dynamicBlocks join 에서 자동 제외)', () => { expect(buildRequirementGraphBlock('고마워!')).toBe(''); }); }); describe('checkRequirementCoverage', () => { const fullMinutes = [ '# 주간회의 회의록', '## 참석자: 김OO, 이OO', '## 결정사항: A안 채택', '## 액션 아이템', '- 견적서 발송 (담당자: 김OO, 기한: 6월 20일까지)', ].join('\n'); it('모든 요소가 있으면 missing 이 빈 배열', () => { const r = checkRequirementCoverage('회의록 정리해줘', fullMinutes); expect(r.ran).toBe(true); expect(r.taskId).toBe('meeting-minutes'); expect(r.missing).toEqual([]); }); it('담당자·기한 누락을 검출한다', () => { const partial = '# 회의록\n참석자: 김OO\n결정사항: A안 채택\n액션 아이템: 견적서 발송'; const r = checkRequirementCoverage('회의록 정리해줘', partial); expect(r.ran).toBe(true); expect(r.missing).toContain('담당자'); expect(r.missing).toContain('기한'); expect(r.covered).toContain('참석자'); }); it('coverageCheck=false 업무(일정)는 검사하지 않는다', () => { const r = checkRequirementCoverage('내일 3시 미팅 잡아줘', '등록했습니다.'); expect(r.ran).toBe(false); }); it('업무 유형 미감지·빈 답변이면 ran=false', () => { expect(checkRequirementCoverage('안녕', '반가워요').ran).toBe(false); expect(checkRequirementCoverage('회의록 정리해줘', ' ').ran).toBe(false); }); }); describe('formatRequirementCoverageFooter', () => { it('누락이 있으면 footer 에 업무명과 누락 요소를 표시', () => { const footer = formatRequirementCoverageFooter({ ran: true, taskId: 'meeting-minutes', taskLabel: '회의록', covered: ['참석자'], missing: ['담당자', '기한'], }); expect(footer).toContain('회의록'); expect(footer).toContain('담당자, 기한'); }); it('전부 충족 또는 미실행이면 빈 문자열 (노이즈 방지)', () => { expect(formatRequirementCoverageFooter({ ran: true, covered: ['참석자'], missing: [] })).toBe(''); expect(formatRequirementCoverageFooter({ ran: false, covered: [], missing: [] })).toBe(''); }); }); describe('DEFAULT_TASK_REQUIREMENTS 무결성', () => { it('모든 detectKeywords / detectPatterns 가 유효한 정규식이다', () => { for (const req of DEFAULT_TASK_REQUIREMENTS) { expect(() => new RegExp(req.detectKeywords.join('|'), 'iu')).not.toThrow(); for (const el of req.elements) { expect(() => new RegExp(el.detectPatterns.join('|'), 'iu')).not.toThrow(); } } }); it('업무 ID 와 요소 ID 가 중복되지 않는다', () => { const taskIds = DEFAULT_TASK_REQUIREMENTS.map((r) => r.id); expect(new Set(taskIds).size).toBe(taskIds.length); for (const req of DEFAULT_TASK_REQUIREMENTS) { const ids = req.elements.map((e) => e.id); expect(new Set(ids).size).toBe(ids.length); } }); });