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

155 lines
6.1 KiB
JavaScript
Raw Normal View History

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