import { detectLessonKind, extractPreventionChecklist, buildLessonChecklistBlock, lessonTemplate, lessonSlug, LESSON_DIR_RE, isQaRegressionFeedback, parseLessonFrontmatter, normalizeLessonTitle, bumpLessonOccurrences, findUnaddressedChecklistItems, } from '../src/retrieval/lessonHelpers'; describe('lessonHelpers.detectLessonKind', () => { it('detects by path segment', () => { expect(detectLessonKind('lessons/2026-05-12-foo.md', '# Foo')).toBe('lesson'); expect(detectLessonKind('docs/playbooks/release.md', 'stuff')).toBe('playbook'); expect(detectLessonKind('qa-findings/bug-x.md', 'stuff')).toBe('qa-finding'); expect(detectLessonKind('a/b/lesson/x.md', '')).toBe('lesson'); // singular "lesson" expect(detectLessonKind('notes/architecture.md', '# Arch')).toBe(''); }); it('frontmatter type wins over path', () => { const fm = '---\ntype: qa-finding\ntitle: x\n---\n# body'; expect(detectLessonKind('notes/whatever.md', fm)).toBe('qa-finding'); const fm2 = '---\ntype: "lesson"\n---\nbody'; expect(detectLessonKind('random.md', fm2)).toBe('lesson'); }); it('ignores non-lesson frontmatter / no frontmatter', () => { expect(detectLessonKind('notes/x.md', '---\ntype: note\n---\nbody')).toBe(''); expect(detectLessonKind('notes/x.md', 'just text, no frontmatter')).toBe(''); expect(detectLessonKind('notes/x.md', '')).toBe(''); }); it('LESSON_DIR_RE matches expected dirs only', () => { expect(LESSON_DIR_RE.test('lessons/x.md')).toBe(true); expect(LESSON_DIR_RE.test('a\\playbooks\\y.md')).toBe(true); expect(LESSON_DIR_RE.test('qa_findings/z.md')).toBe(true); expect(LESSON_DIR_RE.test('lessons-archive/x.md')).toBe(false); // not a path segment boundary expect(LESSON_DIR_RE.test('records/dev.md')).toBe(false); }); }); describe('lessonHelpers.extractPreventionChecklist', () => { it('pulls the bullets under a Prevention Checklist heading', () => { const card = [ '# Lesson: X', '## Root Cause', 'because reasons', '## Prevention Checklist', '- check the allowlist', '* approval flow exists', '- path boundary validated', '## Applies To', '- security', ].join('\n'); expect(extractPreventionChecklist(card)).toEqual([ 'check the allowlist', 'approval flow exists', 'path boundary validated', ]); }); it('returns [] when there is no checklist', () => { expect(extractPreventionChecklist('# Lesson\nsome text')).toEqual([]); expect(extractPreventionChecklist('')).toEqual([]); }); }); describe('lessonHelpers.buildLessonChecklistBlock', () => { it('renders an ACTIVE LESSONS block with checklists when available', () => { const block = buildLessonChecklistBlock([ { title: 'lessons/telegram.md', content: '# X\n## Prevention Checklist\n- allowlist required\n- approval flow' }, { title: 'lessons/paths.md', content: 'no checklist here, just prose about path safety' }, ]); expect(block).toContain('ACTIVE LESSONS'); expect(block).toContain('### lessons/telegram.md'); expect(block).toContain('- [ ] allowlist required'); expect(block).toContain('- [ ] approval flow'); expect(block).toContain('### lessons/paths.md'); expect(block).toContain('just prose about path safety'); expect(block).toContain('END ACTIVE LESSONS'); }); it('returns empty string for no chunks', () => { expect(buildLessonChecklistBlock([])).toBe(''); }); }); describe('lessonHelpers.lessonTemplate / lessonSlug', () => { it('template has frontmatter + the standard sections', () => { const t = lessonTemplate('Telegram 원격 실행은 allowlist 필수', '2026-05-12'); expect(t).toMatch(/^---\ntype: lesson\n/); expect(t).toContain('title: Telegram 원격 실행은 allowlist 필수'); expect(t).toContain('last-seen: 2026-05-12'); for (const h of ['## Situation', '## Mistake / Risk', '## Root Cause', '## Fix', '## Prevention Checklist', '## Applies To']) { expect(t).toContain(h); } // detectLessonKind should recognize the generated template expect(detectLessonKind('lessons/x.md', t)).toBe('lesson'); }); it('slug is filesystem-safe and length-bounded', () => { expect(lessonSlug('Hello, World! / weird:chars')).toBe('hello-world-weird-chars'); expect(lessonSlug('한글 제목 테스트')).toBe('한글-제목-테스트'); expect(lessonSlug('')).toBe('lesson'); expect(lessonSlug('a'.repeat(200)).length).toBeLessThanOrEqual(60); }); }); describe('lessonHelpers.isQaRegressionFeedback', () => { it('flags recurring-problem complaints (ko/en)', () => { expect(isQaRegressionFeedback('이거 또 안 돼')).toBe(true); expect(isQaRegressionFeedback('아까랑 비슷한 실수네')).toBe(true); expect(isQaRegressionFeedback('왜 자꾸 이래?')).toBe(true); expect(isQaRegressionFeedback('고쳤는데 또 깨졌어')).toBe(true); expect(isQaRegressionFeedback('여전히 안 돼')).toBe(true); expect(isQaRegressionFeedback('this is a regression')).toBe(true); expect(isQaRegressionFeedback('why does this keep failing again')).toBe(true); }); it('does not flag ordinary requests', () => { expect(isQaRegressionFeedback('이 함수 리팩터링해줘')).toBe(false); expect(isQaRegressionFeedback('add a new endpoint for users')).toBe(false); expect(isQaRegressionFeedback('')).toBe(false); expect(isQaRegressionFeedback('또')).toBe(false); // too short / not a complaint }); }); describe('lessonHelpers.parseLessonFrontmatter / normalizeLessonTitle / bumpLessonOccurrences', () => { const card = [ '---', 'type: lesson', 'title: Telegram remote exec needs allowlist', 'applies-to: [telegram, "remote-execution", security]', 'occurrences: 2', 'last-seen: 2026-05-01', '---', '# body', 'stuff', ].join('\n'); it('parses frontmatter fields', () => { const fm = parseLessonFrontmatter(card); expect(fm.type).toBe('lesson'); expect(fm.title).toBe('Telegram remote exec needs allowlist'); expect(fm.occurrences).toBe(2); expect(fm.appliesTo).toEqual(['telegram', 'remote-execution', 'security']); }); it('returns {} when there is no frontmatter', () => { expect(parseLessonFrontmatter('# just a heading\ntext')).toEqual({}); expect(parseLessonFrontmatter('')).toEqual({}); }); it('normalizeLessonTitle strips punctuation/case/space for matching', () => { expect(normalizeLessonTitle('Telegram 원격 실행은 allowlist 필수!')).toBe(normalizeLessonTitle('telegram원격실행은allowlist필수')); expect(normalizeLessonTitle('A B C')).toBe('abc'); }); it('bumpLessonOccurrences increments and updates last-seen', () => { const out = bumpLessonOccurrences(card, '2026-05-12'); expect(parseLessonFrontmatter(out).occurrences).toBe(3); expect(out).toContain('last-seen: 2026-05-12'); expect(out).toContain('# body'); // body untouched }); it('bumpLessonOccurrences inserts the keys when missing', () => { const minimal = '---\ntype: lesson\ntitle: X\n---\nbody'; const out = bumpLessonOccurrences(minimal, '2026-05-12'); expect(parseLessonFrontmatter(out).occurrences).toBe(2); expect(out).toContain('last-seen: 2026-05-12'); }); it('bumpLessonOccurrences leaves a frontmatter-less file unchanged', () => { expect(bumpLessonOccurrences('no frontmatter here', '2026-05-12')).toBe('no frontmatter here'); }); }); describe('lessonHelpers.findUnaddressedChecklistItems', () => { const card = [ '# Lesson', '## Prevention Checklist', '- 원격 실행 기능은 allowlist 필수인지 확인한다', '- 파일 변경은 승인 흐름을 거치는지 확인한다', '- <템플릿 placeholder 항목>', '## Applies To', '- security', ].join('\n'); it('flags items whose terms do not appear in the answer', () => { const answer = '이번 변경은 승인 흐름을 거치도록 했습니다. 파일 변경 시 사용자에게 확인을 받습니다.'; const out = findUnaddressedChecklistItems(answer, [card]); // "allowlist" item is unaddressed; the "승인 흐름/파일 변경" item is addressed; the placeholder is skipped expect(out).toEqual(['원격 실행 기능은 allowlist 필수인지 확인한다']); }); it('returns [] when the answer touches all checklist items', () => { const answer = 'allowlist 정책을 추가했고, 파일 변경은 승인 흐름을 거칩니다.'; expect(findUnaddressedChecklistItems(answer, [card])).toEqual([]); }); it('returns [] with no answer or no lessons', () => { expect(findUnaddressedChecklistItems('', [card])).toEqual([]); expect(findUnaddressedChecklistItems('something', [])).toEqual([]); }); });