chore: version up to 2.80.35 and package with experience memory

This commit is contained in:
g1nation
2026-05-12 23:23:23 +09:00
parent 065e598cca
commit f6b27a125b
25 changed files with 1088 additions and 103 deletions
+191
View File
@@ -0,0 +1,191 @@
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([]);
});
});