// tools/skill-scope-verifier.test.mjs import { describe, it, expect } from 'vitest'; import { V4_1_SCOPE_THRESHOLDS, initTracker, updateTracker, evaluateScope, contentScopeCheck, maxCallsCheck, } from './skill-scope-verifier.mjs'; const writingPlans = { expected_tools: ['Read', 'Grep', 'AskUserQuestion', 'Write', 'TodoWrite'], expected_output_glob: 'docs/superpowers/plans/*.md', content_scope: { Edit: { allowed_globs: ['docs/superpowers/plans/*.md'], max_count: 5 }, Write: { allowed_globs: ['docs/superpowers/plans/*.md'], max_count: 3 }, }, max_tool_calls_post_skill: 30, }; function mk() { return initTracker({ skillInvoked: 'superpowers:writing-plans', turnId: 't1', toolUseIdOfSkill: 'u1', scopeConfig: writingPlans, tsSkillInvokedIso: '2026-05-29T00:00:00Z', }); } describe('initTracker', () => { it('starts with zeroed counters', () => { const t = mk(); expect(t.tool_count_since_skill).toBe(0); expect(t.off_scope_count).toBe(0); expect(t.expected_output_written).toBe(false); expect(t.skill_invoked).toBe('superpowers:writing-plans'); }); }); describe('updateTracker', () => { it('counts in-scope tool without off_scope', () => { const t = updateTracker(mk(), 'Read', {}); expect(t.tool_count_since_skill).toBe(1); expect(t.off_scope_count).toBe(0); }); it('counts off-scope tool', () => { const t = updateTracker(mk(), 'Bash', { command: 'ls' }); expect(t.off_scope_count).toBe(1); }); it('sets expected_output_written on matching Write', () => { const t = updateTracker(mk(), 'Write', { file_path: 'docs/superpowers/plans/x.md' }); expect(t.expected_output_written).toBe(true); }); }); describe('evaluateScope', () => { it('allows when below min_tools=3', () => { const t = { ...mk(), tool_count_since_skill: 2, off_scope_count: 2 }; expect(evaluateScope(t).action).toBe('allow'); }); it('hard_block at ratio 0.30 (3/10)', () => { const t = { ...mk(), tool_count_since_skill: 10, off_scope_count: 3 }; expect(evaluateScope(t).action).toBe('hard_block'); }); it('soft_flag at ratio 0.20 (2/10)', () => { const t = { ...mk(), tool_count_since_skill: 10, off_scope_count: 2 }; expect(evaluateScope(t).action).toBe('soft_flag'); }); it('allow at ratio 0.10 (1/10)', () => { const t = { ...mk(), tool_count_since_skill: 10, off_scope_count: 1 }; expect(evaluateScope(t).action).toBe('allow'); }); it('hard_block at ratio 0.5 (2/4)', () => { const t = { ...mk(), tool_count_since_skill: 4, off_scope_count: 2 }; expect(evaluateScope(t).action).toBe('hard_block'); }); }); describe('contentScopeCheck', () => { it('allows Write to matching glob', () => { expect(contentScopeCheck('Write', 'docs/superpowers/plans/x.md', writingPlans).action).toBe('allow'); }); it('hard_block Edit to off-domain file', () => { expect(contentScopeCheck('Edit', 'app/Models/User.php', writingPlans).action).toBe('hard_block'); }); it('allows Read (not a write-tool)', () => { expect(contentScopeCheck('Read', 'app/Models/User.php', writingPlans).action).toBe('allow'); }); it('allows Write when no content_scope defined', () => { expect(contentScopeCheck('Write', 'x.md', { expected_tools: [] }).action).toBe('allow'); }); it('allows Edit to matching test glob', () => { const tdd = { content_scope: { Edit: { allowed_globs: ['**/*.test.*', 'tests/**'] } } }; expect(contentScopeCheck('Edit', 'app/Foo.test.mjs', tdd).action).toBe('allow'); }); it('hard_block Edit to non-test file', () => { const tdd = { content_scope: { Edit: { allowed_globs: ['**/*.test.*', 'tests/**'] } } }; expect(contentScopeCheck('Edit', 'app/Foo.php', tdd).action).toBe('hard_block'); }); }); describe('maxCallsCheck', () => { it('hard_block when tool_count >= max and no expected output written', () => { const t = { ...mk(), tool_count_since_skill: 30, expected_output_written: false }; expect(maxCallsCheck(t).action).toBe('hard_block'); }); it('allows when expected_output_written even at max calls', () => { const t = { ...mk(), tool_count_since_skill: 30, expected_output_written: true }; expect(maxCallsCheck(t).action).toBe('allow'); }); it('allows when below max calls', () => { const t = { ...mk(), tool_count_since_skill: 5, expected_output_written: false }; expect(maxCallsCheck(t).action).toBe('allow'); }); }); describe('reason strings', () => { it('evaluateScope hard_block reason contains "invoke matching Skill"', () => { const t = { ...mk(), tool_count_since_skill: 10, off_scope_count: 3 }; const r = evaluateScope(t); expect(r.action).toBe('hard_block'); expect(r.reason).toContain('invoke matching Skill'); }); it('contentScopeCheck hard_block reason contains "не соответствует"', () => { const r = contentScopeCheck('Edit', 'app/Models/User.php', writingPlans); expect(r.action).toBe('hard_block'); expect(r.reason).toContain('не соответствует'); }); it('maxCallsCheck hard_block reason contains "Skill scope exceeded"', () => { const t = { ...mk(), tool_count_since_skill: 30, expected_output_written: false }; const r = maxCallsCheck(t); expect(r.action).toBe('hard_block'); expect(r.reason).toContain('Skill scope exceeded'); }); }); describe('updateTracker — accumulation', () => { it('chain Read×3 + Bash×2 → ratio 0.4 → hard_block', () => { let t = mk(); t = updateTracker(t, 'Read', {}); t = updateTracker(t, 'Read', {}); t = updateTracker(t, 'Read', {}); t = updateTracker(t, 'Bash', {}); t = updateTracker(t, 'Bash', {}); expect(t.tool_count_since_skill).toBe(5); expect(t.off_scope_count).toBe(2); expect(evaluateScope(t).action).toBe('hard_block'); }); it('off_scope never increments when expected_tools is empty', () => { const tracker = initTracker({ skillInvoked: 'foo', turnId: 't2', toolUseIdOfSkill: 'u2', scopeConfig: { expected_tools: [], max_tool_calls_post_skill: 100 }, tsSkillInvokedIso: '2026-05-29T00:00:00Z', }); const t = updateTracker(updateTracker(tracker, 'Bash', {}), 'Edit', {}); expect(t.off_scope_count).toBe(0); }); });