Files
meeting-minutes/src/__tests__/templates.test.ts
T
9af00c3016 feat: 회의록 섹션을 내용 기반으로 생성하고 액션 아이템에 담당자 표기 (#13)
고정 제목(## 주요 논의 사항)을 강요하던 프롬프트를, 회의가 실제로 다룬
주제에서 섹션 제목을 도출하도록 바꿨다. 제목만 훑어도 내용이 파악된다.

- meeting·one_on_one: 본문 섹션 제목을 AI가 직접 짓도록 지시
- brainstorm: 카테고리도 미리 정해진 목록이 아님을 명시
- 액션 아이템에 (담당자)·기한 표기 요청 + 예시 제공
- COMMON_RULES 신설 — 환각 방지, 발화자 표기 활용,
  영어 기술 용어 원문 유지(한영 코드 스위칭 대응). 프리셋에만 적용하고
  custom 프롬프트는 사용자가 전적으로 제어하도록 유지
- LIVE_MODIFIER에 섹션 제목이 갱신될 수 있다는 안내 추가

버그 수정: 기존 meeting 프롬프트는 액션 아이템을 "담당자와 기한이 명확한
항목만"으로 제한해, 담당자를 정하지 않은 회의에서는 액션 아이템이 하나도
추출되지 않았다. 담당자가 불분명해도 포함하도록 변경.

`- [ ]` 체크박스 형식은 그대로 유지해 parseActionItems와 호환된다.
테스트 102 → 113.

Co-authored-by: csbae <csbae@RP-002.local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:17:22 +09:00

283 lines
9.0 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
TEMPLATES,
buildPrompt,
resolveDepth,
liveEnabledFor,
depthAdjustableFor,
DEFAULT_TEMPLATE_ID,
DEFAULT_DEPTH,
} from '@/lib/templates'
describe('TEMPLATES registry', () => {
it('6개의 프리셋 템플릿을 제공한다', () => {
expect(Object.keys(TEMPLATES)).toHaveLength(6)
expect(TEMPLATES).toHaveProperty('meeting')
expect(TEMPLATES).toHaveProperty('lecture')
expect(TEMPLATES).toHaveProperty('one_on_one')
expect(TEMPLATES).toHaveProperty('brainstorm')
expect(TEMPLATES).toHaveProperty('interview')
expect(TEMPLATES).toHaveProperty('raw')
})
it('기본 템플릿은 meeting, 기본 강도는 standard', () => {
expect(DEFAULT_TEMPLATE_ID).toBe('meeting')
expect(DEFAULT_DEPTH).toBe('standard')
})
it('raw 템플릿은 라이브 요약과 강도 조절이 불가', () => {
expect(TEMPLATES.raw.liveSupported).toBe(false)
expect(TEMPLATES.raw.depthAdjustable).toBe(false)
})
it('interview 템플릿은 라이브 요약 불가', () => {
expect(TEMPLATES.interview.liveSupported).toBe(false)
})
it('lecture는 detailed, meeting은 standard 기본 강도', () => {
expect(TEMPLATES.lecture.defaultDepth).toBe('detailed')
expect(TEMPLATES.meeting.defaultDepth).toBe('standard')
})
})
describe('buildPrompt', () => {
const transcript = '안녕하세요 테스트입니다'
it('meeting + standard + non-live 조합의 프롬프트를 생성한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
})
expect(prompt).toContain('회의록 작성 전문가')
expect(prompt).toContain('액션 아이템')
expect(prompt).toContain('표준')
expect(prompt).toContain(transcript)
expect(prompt).not.toContain('회의가 아직 진행 중')
})
it('live=true이면 진행 중 모디파이어를 포함한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
live: true,
})
expect(prompt).toContain('회의가 아직 진행 중')
expect(prompt).toContain('논의 중')
})
it('depthAdjustable=false인 raw 템플릿은 강도 모디파이어를 적용하지 않는다', () => {
const prompt = buildPrompt({
templateId: 'raw',
depth: 'concise',
transcript,
})
expect(prompt).toContain('편집자')
expect(prompt).not.toContain('작성 강도')
})
it('liveSupported=false 템플릿은 live=true여도 라이브 모디파이어를 넣지 않는다', () => {
const prompt = buildPrompt({
templateId: 'interview',
depth: 'standard',
transcript,
live: true,
})
expect(prompt).not.toContain('회의가 아직 진행 중')
})
it('강도별로 다른 모디파이어가 포함된다', () => {
const concise = buildPrompt({
templateId: 'meeting',
depth: 'concise',
transcript,
})
const detailed = buildPrompt({
templateId: 'meeting',
depth: 'detailed',
transcript,
})
expect(concise).toContain('간결')
expect(detailed).toContain('상세')
expect(concise).not.toContain('상세')
})
it('custom 템플릿은 customPrompt를 그대로 사용한다', () => {
const prompt = buildPrompt({
templateId: 'custom',
depth: 'standard',
transcript,
customPrompt: '당신은 시인입니다. 시 형식으로 정리하세요.',
})
expect(prompt).toContain('당신은 시인입니다')
expect(prompt).toContain(transcript)
expect(prompt).not.toContain('액션 아이템')
})
it('custom인데 customPrompt가 비어있으면 meeting 기본 프롬프트로 폴백', () => {
const prompt = buildPrompt({
templateId: 'custom',
depth: 'standard',
transcript,
customPrompt: ' ',
})
expect(prompt).toContain('회의록 작성 전문가')
})
})
describe('resolveDepth', () => {
it('depth 미지정 시 템플릿 기본값을 반환한다', () => {
expect(resolveDepth('lecture')).toBe('detailed')
expect(resolveDepth('meeting')).toBe('standard')
})
it('depthAdjustable=false 템플릿은 전달된 depth를 무시', () => {
expect(resolveDepth('raw', 'concise')).toBe('detailed')
})
it('지정된 depth가 있고 조절 가능하면 그대로 반환', () => {
expect(resolveDepth('meeting', 'concise')).toBe('concise')
})
})
describe('liveEnabledFor / depthAdjustableFor', () => {
it('raw는 라이브/강도 둘 다 false', () => {
expect(liveEnabledFor('raw')).toBe(false)
expect(depthAdjustableFor('raw')).toBe(false)
})
it('interview는 라이브 false, 강도 true', () => {
expect(liveEnabledFor('interview')).toBe(false)
expect(depthAdjustableFor('interview')).toBe(true)
})
it('meeting / lecture / brainstorm / one_on_one은 둘 다 true', () => {
expect(liveEnabledFor('meeting')).toBe(true)
expect(liveEnabledFor('lecture')).toBe(true)
expect(liveEnabledFor('brainstorm')).toBe(true)
expect(liveEnabledFor('one_on_one')).toBe(true)
})
it('custom은 라이브 가능, 강도 조절은 불가(사용자 프롬프트가 강도까지 지정)', () => {
expect(liveEnabledFor('custom')).toBe(true)
expect(depthAdjustableFor('custom')).toBe(false)
})
})
describe('내용 기반 섹션 구조', () => {
const transcript = '이번에는 가격 비교 사이트를 만들어 봅시다'
it('meeting은 섹션 제목을 직접 짓도록 지시한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
})
expect(prompt).toContain('섹션 제목을 직접 지어서')
// 고정 제목을 쓰지 말라는 지시가 함께 있어야 한다
expect(prompt).toContain('일반적인 제목은 쓰지 마세요')
})
it('one_on_one도 주제별 제목을 직접 짓도록 지시한다', () => {
const prompt = buildPrompt({
templateId: 'one_on_one',
depth: 'standard',
transcript,
})
expect(prompt).toContain('제목을 직접 지어')
})
it('raw는 구조적 제목을 덧붙이지 말라는 지시를 유지한다', () => {
const prompt = buildPrompt({
templateId: 'raw',
depth: 'detailed',
transcript,
})
expect(prompt).toContain('구조적 제목(## 섹션)을 덧붙이지 마세요')
})
})
describe('액션 아이템 담당자 표기', () => {
const transcript = '다음 주까지 정리해 주세요'
it('담당자 표기를 요구한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
})
expect(prompt).toContain('(담당자)')
})
it('담당자가 불분명해도 항목을 버리지 않도록 지시한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
})
expect(prompt).toContain('담당자가 불분명해도')
})
it('체크박스 형식을 유지해 액션 아이템 파서와 호환된다', () => {
for (const id of ['meeting', 'one_on_one', 'brainstorm'] as const) {
const prompt = buildPrompt({ templateId: id, depth: 'standard', transcript })
expect(prompt).toContain('- [ ]')
}
})
})
describe('공통 규칙', () => {
const transcript = '테스트 발화'
it('프리셋 템플릿 전체에 공통 규칙이 붙는다', () => {
for (const id of Object.keys(TEMPLATES) as (keyof typeof TEMPLATES)[]) {
const prompt = buildPrompt({ templateId: id, depth: 'standard', transcript })
expect(prompt).toContain('지어내지 마세요')
expect(prompt).toContain('한국어로 작성하세요')
}
})
it('발화자 표기가 있으면 활용하도록 지시한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
})
expect(prompt).toContain('발화자 표기가 있다면')
})
it('영어 기술 용어는 원문 표기를 유지하도록 지시한다', () => {
const prompt = buildPrompt({
templateId: 'meeting',
depth: 'standard',
transcript,
})
expect(prompt).toContain('원문 표기를 유지하세요')
})
it('custom 프롬프트에는 공통 규칙을 덧붙이지 않는다', () => {
const prompt = buildPrompt({
templateId: 'custom',
depth: 'standard',
transcript,
customPrompt: '한 줄로만 요약해줘',
})
expect(prompt).toContain('한 줄로만 요약해줘')
expect(prompt).not.toContain('지어내지 마세요')
})
it('custom 프롬프트가 비어 있으면 meeting 템플릿 + 공통 규칙으로 대체한다', () => {
const prompt = buildPrompt({
templateId: 'custom',
depth: 'standard',
transcript,
customPrompt: ' ',
})
expect(prompt).toContain('회의록 작성 전문가')
expect(prompt).toContain('지어내지 마세요')
// 기존 동작 유지 — custom 경로에는 강도/라이브 모디파이어를 적용하지 않는다
expect(prompt).not.toContain('작성 강도')
})
})