397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
5.5 KiB
JavaScript
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);
|
|
});
|
|
});
|