import { describe, it, expect } from 'vitest'; import { extractSkillMentions, extractSkillToolCalls, skillNameMatches, verifyClaims, detectErasure, hardSyncCheck, } from './todowrite-skill-verifier.mjs'; describe('extractSkillMentions', () => { it('extracts superpowers: mention', () => { const m = extractSkillMentions([{ content: 'invoke superpowers:writing-plans', status: 'pending' }]); expect(m.some((x) => x.skill_name === 'superpowers:writing-plans')).toBe(true); }); it('extracts Skill() syntax', () => { const m = extractSkillMentions([{ content: 'call Skill(brain-retro)', status: 'completed' }]); expect(m.some((x) => x.skill_name === 'brain-retro')).toBe(true); }); it('returns empty for plain text', () => { expect(extractSkillMentions([{ content: 'fix the parser', status: 'pending' }])).toEqual([]); }); }); describe('extractSkillToolCalls', () => { it('finds Skill tool_use entries', () => { const t = [{ type: 'tool_use', name: 'Skill', id: 'u1', input: { skill: 'superpowers:writing-plans' } }]; const c = extractSkillToolCalls(t); expect(c[0].skill_name).toBe('superpowers:writing-plans'); }); }); describe('skillNameMatches', () => { it('matches writing-plans to superpowers:writing-plans (suffix)', () => { expect(skillNameMatches('writing-plans', 'superpowers:writing-plans')).toBe(true); }); it('matches exact full name', () => { expect(skillNameMatches('superpowers:writing-plans', 'superpowers:writing-plans')).toBe(true); }); it('does not match different skills', () => { expect(skillNameMatches('brain-retro', 'superpowers:writing-plans')).toBe(false); }); }); describe('verifyClaims', () => { it('flags mention with no actual call', () => { const mentions = [{ skill_name: 'superpowers:writing-plans', status: 'pending', text: 'x' }]; expect(verifyClaims(mentions, []).mismatches.length).toBe(1); }); it('no mismatch when actual call exists', () => { const mentions = [{ skill_name: 'superpowers:writing-plans', status: 'pending', text: 'x' }]; const actual = [{ skill_name: 'superpowers:writing-plans' }]; expect(verifyClaims(mentions, actual).mismatches.length).toBe(0); }); it('matches suffix (writing-plans vs superpowers:writing-plans)', () => { const mentions = [{ skill_name: 'writing-plans', status: 'pending', text: 'x' }]; const actual = [{ skill_name: 'superpowers:writing-plans' }]; expect(verifyClaims(mentions, actual).mismatches.length).toBe(0); }); }); describe('detectErasure', () => { it('detects removed item with skill mention', () => { const snaps = [{ ts: 't', diff: { removed: [{ content: 'invoke superpowers:writing-plans', status: 'pending' }] } }]; expect(detectErasure(snaps).erased.length).toBe(1); }); it('ignores removed items without skill mentions', () => { const snaps = [{ ts: 't', diff: { removed: [{ content: 'fix bug', status: 'done' }] } }]; expect(detectErasure(snaps).erased.length).toBe(0); }); it('handles snapshot without diff gracefully', () => { expect(detectErasure([{ ts: 't' }]).erased.length).toBe(0); }); }); describe('hardSyncCheck — v4.1', () => { it('blocks completed item claiming skill with no actual call', () => { const mentions = [{ skill_name: 'superpowers:writing-plans', text: 'plan', status: 'completed' }]; expect(hardSyncCheck(mentions, []).action).toBe('hard_block_next_mutating'); }); it('allows completed item when matching call exists', () => { const mentions = [{ skill_name: 'superpowers:writing-plans', text: 'plan', status: 'completed' }]; const actual = [{ skill_name: 'superpowers:writing-plans' }]; expect(hardSyncCheck(mentions, actual).action).toBe('allow'); }); it('allows pending item even if no actual call', () => { const mentions = [{ skill_name: 'superpowers:writing-plans', text: 'plan', status: 'pending' }]; expect(hardSyncCheck(mentions, []).action).toBe('allow'); }); it('blocks on second mention if first ok but second completed+missing', () => { const actual = [{ skill_name: 'superpowers:writing-plans' }]; const mentions = [ { skill_name: 'superpowers:writing-plans', text: 'plan', status: 'completed' }, { skill_name: 'superpowers:brain-retro', text: 'retro', status: 'completed' }, ]; expect(hardSyncCheck(mentions, actual).action).toBe('hard_block_next_mutating'); }); }); describe('extractSkillMentions — dedup', () => { it('deduplicates same skill mentioned twice in one item', () => { const m = extractSkillMentions([{ content: 'invoke superpowers:writing-plans then invoke superpowers:writing-plans', status: 'pending', }]); const names = m.map((x) => x.skill_name); expect(names.filter((n) => n === 'superpowers:writing-plans').length).toBe(1); }); }); describe('extractSkillMentions — cyrillic patterns', () => { it('вызови pattern detects skill mention', () => { const m = extractSkillMentions([{ content: 'вызови brain-retro', status: 'pending' }]); expect(m.some((x) => x.skill_name === 'brain-retro')).toBe(true); }); it('делай pattern detects skill mention', () => { const m = extractSkillMentions([{ content: 'делай subagent-driven-development', status: 'pending' }]); expect(m.some((x) => x.skill_name === 'subagent-driven-development')).toBe(true); }); it('no match inside longer word (перевызови)', () => { const m = extractSkillMentions([{ content: 'перевызови foo', status: 'pending' }]); expect(m.some((x) => x.skill_name === 'foo')).toBe(false); }); });