Files
connectai/tests/meetPrompt.test.ts
T
koriweb 6dc5f17dec v2.2.258: /meet 화자 팀/역할 정규화 + 헤더 전조각 주입 + 검증 5종
STT 화자번호(참석자 N) 박멸→회사 표준 팀/역할 귀속, 회의 헤더 전 청크 주입, 전역 헤드라인 추출, 결정 게이트·담화 상태 태깅, 슬림 6섹션 포맷, 타임스탬프 근거, parseActionItems 헤더명 기반 재작성, 검증 패스 5종. 전체 698 통과.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:10:32 +09:00

76 lines
3.6 KiB
TypeScript

/**
* Regression guard for the /meet prompt policy (v2.2.258).
*
* After a real meeting record review, the policy shifted to:
* 1) STT speaker numbers ("참석자 N") must NEVER appear in output — speakers are
* normalized to team/role instead (individual names only when certain).
* 2) empty cells are "—", not guessed placeholders; fully-undecided → open issue.
* 3) 근거 is a timestamp [mm:ss], not a raw STT quote.
* 4) decision vs action boundary: 일감→action table, 결정→pure direction only.
* 5) rejected/withdrawn hypotheses must not be promoted to issues/decisions.
*
* Prompts can't be unit-tested for model behavior, but we CAN guard that the
* policy text doesn't silently get dropped in a future refactor.
*/
import {
buildMeetPrompt,
buildMeetExtractPrompt,
buildMeetReducePrompt,
} from '../src/features/datacollect/prompts/meetPrompt';
const transcript = '참석자 1 00:01 이거는 이렇게 이렇게 해주세요. 참석자 2 00:10 네 수정할게요.';
const metadata = '회의명: 테스트';
describe('/meet prompt — speaker normalization & slim format policy', () => {
// The OUTPUT_FORMAT block is shared by the single-shot and reduce paths, so
// both must carry the policy.
const sharedFormatBuilders: Array<[string, string]> = [
['buildMeetPrompt', buildMeetPrompt(transcript, metadata)],
['buildMeetReducePrompt', buildMeetReducePrompt('## 액션\n- [넥서스개발팀] 작업', metadata)],
];
test.each(sharedFormatBuilders)('%s bans "참석자 N" tokens in output', (_name, prompt) => {
expect(prompt).toMatch(/"참석자 N"/);
expect(prompt).toMatch(/토큰 0개|절대 금지|절대 쓰지/);
});
test.each(sharedFormatBuilders)('%s attributes speakers by team/role', (_name, prompt) => {
expect(prompt).toMatch(/팀\/역할/);
});
test.each(sharedFormatBuilders)('%s requires timestamp 근거 ([mm:ss])', (_name, prompt) => {
expect(prompt).toMatch(/\[mm:ss\]/);
});
test.each(sharedFormatBuilders)('%s separates decision from action', (_name, prompt) => {
expect(prompt).toMatch(/결정 ↔ 액션 경계|순수 방향\/정책|결정\/액션 경계/);
});
test.each(sharedFormatBuilders)('%s blanks unknown cells with "—" instead of guessing', (_name, prompt) => {
expect(prompt).toMatch(/빈 칸은 "—"/);
});
test('extract (map) stage normalizes speakers to role, tags dialectic state & timestamps', () => {
const p = buildMeetExtractPrompt('참석자 1 00:01 이거 이렇게요', metadata, 1, 3);
expect(p).toMatch(/팀\/역할/); // role mapping
expect(p).toMatch(/\[mm:ss\]/); // timestamp grounding
expect(p).toMatch(/반박됨|철회됨/); // dialectic state tags
expect(p).toMatch(/안건\/주제/); // agenda coverage list
expect(p).toContain('[내용 확인필요]'); // content-empty deictic guard retained
});
test('reduce stage extracts a global headline & dedups across chunks', () => {
const p = buildMeetReducePrompt('## 액션\n- [기획] 작업', metadata);
expect(p).toMatch(/전역 헤드라인/);
expect(p).toMatch(/dedup|병합/);
expect(p).toMatch(/반박됨|폐기/); // rejected hypotheses not promoted
});
test('final checklist references the new gates', () => {
const prompt = buildMeetPrompt(transcript, metadata);
expect(prompt).toMatch(/"참석자 N" 토큰 0개/);
expect(prompt).toMatch(/빈 칸은 "—"/);
});
});