Files
brain/tools/todowrite-skill-verifier.test.mjs
T

121 lines
5.5 KiB
JavaScript

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);
});
});