Files
brain/tools/skill-scope-verifier.test.mjs

155 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});
});