// tools/self-debrief-detector.test.mjs import { describe, it, expect } from 'vitest'; import { SELF_DEBRIEF_PATTERNS, extractSkillCallsLastNTurns, detectSelfDebrief, } from './self-debrief-detector.mjs'; const noSkills = []; describe('detectSelfDebrief — patterns', () => { it('blocks "я проанализировал свои паттерны" without recent self-retro', () => { const r = detectSelfDebrief('Я проанализировал свои обходные паттерны и понял...', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('blocks "Generalisable lesson" without self-retro', () => { const r = detectSelfDebrief('Generalisable lesson: always check first.', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('blocks "Lesson v3.7:" version pattern', () => { const r = detectSelfDebrief('Lesson v3.7: the boundaries had holes.', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('allows plain response with no self-debrief language', () => { const r = detectSelfDebrief('Я починил баг в парсере, тесты зелёные.', noSkills); expect(r.action).toBe('allow'); }); }); describe('detectSelfDebrief — self-retro escape', () => { it('allows debrief content when brain-retro skill was invoked recently', () => { const transcript = [{ type: 'tool_use', name: 'Skill', input: { skill: 'brain-retro' }, turn: 1 }]; const r = detectSelfDebrief('Я обобщил выводы по эпизодам.', transcript, { recentTurns: 30 }); expect(r.action).toBe('allow'); }); }); describe('SELF_DEBRIEF_PATTERNS — each pattern', () => { it('pattern 2/5: blocks "self-retrospect анализ"', () => { const r = detectSelfDebrief('self-retrospect анализ выполнен', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('pattern 3: blocks "обобщаю опыт сессии"', () => { const r = detectSelfDebrief('обобщаю опыт сессии и делаю выводы', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('pattern 4: blocks "я заметил паттерн в своих решениях"', () => { const r = detectSelfDebrief('я заметил паттерн в своих решениях', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('pattern 5: blocks "делаю self-retro по эпизодам"', () => { const r = detectSelfDebrief('делаю self-retro по эпизодам', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('pattern 6: blocks "брэйн-ретро показал"', () => { const r = detectSelfDebrief('брэйн-ретро показал интересные результаты', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('pattern 7: blocks "generalized lesson from this"', () => { const r = detectSelfDebrief('generalized lesson from this experience', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); it('pattern 8: blocks "урок v4.1: границы имели дыры"', () => { const r = detectSelfDebrief('урок v4.1: границы имели дыры', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); }); describe('detectSelfDebrief — cyrillic false-positive prevention (unicode lookbehind)', () => { it('allows "Судья проанализировал свои паттерны поведения." — судья ends in я, not a sentence start', () => { const r = detectSelfDebrief('Судья проанализировал свои паттерны поведения.', noSkills); expect(r.action).toBe('allow'); }); it('allows "Семья заметил паттерн." — семья ends in я, mid-word match prevented', () => { const r = detectSelfDebrief('Семья заметил паттерн.', noSkills); expect(r.action).toBe('allow'); }); it('still hard_blocks "я проанализировал свои паттерны" at string start — lookbehind at pos 0 succeeds', () => { const r = detectSelfDebrief('я проанализировал свои паттерны', noSkills); expect(r.action).toBe('hard_block_next_mutating'); }); }); describe('extractSkillCallsLastNTurns', () => { it('transcript without turn fields — 1 Skill call returned (whole transcript scope)', () => { const transcript = [{ type: 'tool_use', name: 'Skill', input: { skill: 'brain-retro' } }]; const result = extractSkillCallsLastNTurns(transcript, 30); expect(result).toHaveLength(1); expect(result[0].skill_name).toBe('brain-retro'); }); it('Skill call at turn 5, n=30, maxTurn=40 — turn 5 ≤ 10 excluded → []', () => { const transcript = [ { type: 'tool_use', name: 'Skill', input: { skill: 'brain-retro' }, turn: 5 }, { type: 'tool_result', name: 'other', turn: 40 }, ]; const result = extractSkillCallsLastNTurns(transcript, 30); expect(result).toHaveLength(0); }); it('Skill call at turn 35, n=30, maxTurn=40 — turn 35 > 10, included', () => { const transcript = [ { type: 'tool_use', name: 'Skill', input: { skill: 'self-retrospect' }, turn: 35 }, { type: 'tool_result', name: 'other', turn: 40 }, ]; const result = extractSkillCallsLastNTurns(transcript, 30); expect(result).toHaveLength(1); expect(result[0].skill_name).toBe('self-retrospect'); }); }); describe('detectSelfDebrief — self-retro skill not matching', () => { it('transcript with Skill(superpowers:writing-plans) only → debrief text → hard_block_next_mutating', () => { const transcript = [{ type: 'tool_use', name: 'Skill', input: { skill: 'superpowers:writing-plans' }, turn: 1 }]; const r = detectSelfDebrief('self-retrospect анализ выполнен', transcript, { recentTurns: 30 }); expect(r.action).toBe('hard_block_next_mutating'); }); it('transcript with Skill(self-retrospect) → debrief text → allow', () => { const transcript = [{ type: 'tool_use', name: 'Skill', input: { skill: 'self-retrospect' }, turn: 1 }]; const r = detectSelfDebrief('Generalisable lesson: always check.', transcript, { recentTurns: 30 }); expect(r.action).toBe('allow'); }); it('custom opts.selfRetroSkills:[custom-retro] + Skill(custom-retro) → allow', () => { const transcript = [{ type: 'tool_use', name: 'Skill', input: { skill: 'custom-retro' }, turn: 1 }]; const r = detectSelfDebrief('Generalisable lesson: check.', transcript, { recentTurns: 30, selfRetroSkills: ['custom-retro'], }); expect(r.action).toBe('allow'); }); });