/** * 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(/빈 칸은 "—"/); }); });