import { describe, it, expect } from 'vitest'; import { DEFAULT_MCP_CLASSIFICATION, matchClassificationKey, classifyMcpTool, } from './mcp-tool-classifier.mjs'; describe('DEFAULT_MCP_CLASSIFICATION', () => { it('is frozen', () => { expect(Object.isFrozen(DEFAULT_MCP_CLASSIFICATION)).toBe(true); }); it('has a default fallback of block', () => { expect(DEFAULT_MCP_CLASSIFICATION.default).toBe('block'); }); it('includes v4.1 WebSearch / WebFetch entries (G1)', () => { expect(DEFAULT_MCP_CLASSIFICATION.WebSearch).toBeTruthy(); expect(DEFAULT_MCP_CLASSIFICATION.WebFetch).toBeTruthy(); }); it('database-query carries a full-statement scan (G12)', () => { const dq = DEFAULT_MCP_CLASSIFICATION['mcp__laravel-boost__database-query']; expect(dq.query_full_statement_scan).toBeTruthy(); }); it('classifies research-tooling families (perplexity/exa/firecrawl) as read_only (allow)', () => { expect(classifyMcpTool('mcp__perplexity__perplexity_ask', { messages: [] }).decision).toBe('allow'); expect(classifyMcpTool('mcp__exa__web_search_exa', { query: 'x' }).decision).toBe('allow'); expect(classifyMcpTool('mcp__firecrawl__firecrawl_scrape', { url: 'https://example.com' }).decision).toBe('allow'); }); }); describe('matchClassificationKey', () => { const map = { 'mcp__redis__get': { category: 'read_only' }, 'mcp__redis__set': { category: 'hard_blacklist' }, 'mcp__github__list_*': { category: 'read_only' }, 'mcp__laravel-boost__database-query': { category: 'conditional' }, 'mcp__laravel-boost__*': { category: 'read_only' }, 'mcp__plugin_*_*__authenticate': { category: 'hard_blacklist' }, 'default': 'block', }; it('prefers an exact key over a glob key (most specific wins)', () => { expect(matchClassificationKey('mcp__laravel-boost__database-query', map).category).toBe('conditional'); }); it('falls back to the glob key when no exact match', () => { expect(matchClassificationKey('mcp__laravel-boost__list-tables', map).category).toBe('read_only'); }); it('matches single-segment glob', () => { expect(matchClassificationKey('mcp__github__list_branches', map).category).toBe('read_only'); }); it('matches multi-wildcard plugin auth glob', () => { expect(matchClassificationKey('mcp__plugin_marketing_hubspot__authenticate', map).category).toBe('hard_blacklist'); }); it('returns null when nothing matches (caller applies default)', () => { expect(matchClassificationKey('mcp__unknown__thing', map)).toBeNull(); }); it('never matches the literal "default" key as a tool', () => { expect(matchClassificationKey('default', map)).toBeNull(); }); }); describe('classifyMcpTool — simple categories', () => { it('allows read_only', () => { expect(classifyMcpTool('mcp__redis__get', {}).decision).toBe('allow'); }); it('blocks hard_blacklist', () => { expect(classifyMcpTool('mcp__redis__set', { key: 'x' }).decision).toBe('block'); }); it('blocks unknown tool via default (fail-CLOSE)', () => { const r = classifyMcpTool('mcp__unknown__thing', {}); expect(r.decision).toBe('block'); expect(r.reason).toMatch(/not in.*classification/i); }); }); describe('classifyMcpTool — database-query full-statement scan (G12)', () => { it('allows a plain SELECT', () => { expect(classifyMcpTool('mcp__laravel-boost__database-query', { query: 'SELECT * FROM users' }).decision).toBe('allow'); }); it('blocks a mutating verb anywhere (combined SELECT;UPDATE — T82)', () => { const r = classifyMcpTool('mcp__laravel-boost__database-query', { query: 'SELECT 1; UPDATE users SET x=1' }); expect(r.decision).toBe('block'); }); it('blocks UPDATE even when it does not start the statement', () => { const r = classifyMcpTool('mcp__laravel-boost__database-query', { query: ' /*c*/ UPDATE t SET a=1' }); expect(r.decision).toBe('block'); }); it('asks when neither read-only nor blocked matched', () => { const r = classifyMcpTool('mcp__laravel-boost__database-query', { query: 'PRAGMA foo' }); expect(r.decision).toBe('ask'); }); }); describe('classifyMcpTool — path_args (create_or_update_file)', () => { it('blocks when path arg is protected (injected predicate)', () => { const deps = { isProtectedPath: (p) => p.includes('claude.md') }; const r = classifyMcpTool('mcp__github__create_or_update_file', { path: 'CLAUDE.md' }, deps); expect(r.decision).toBe('block'); }); it('hard_blacklist category still blocks regardless of path (base category)', () => { const r = classifyMcpTool('mcp__github__create_or_update_file', { path: 'safe.txt' }); expect(r.decision).toBe('block'); }); }); describe('classifyMcpTool — URL whitelist (WebFetch / browser_navigate)', () => { it('allows whitelisted WebFetch URL', () => { expect(classifyMcpTool('WebFetch', { url: 'https://docs.anthropic.com/x' }).decision).toBe('allow'); }); it('blocks non-whitelisted WebFetch URL', () => { expect(classifyMcpTool('WebFetch', { url: 'https://evil.example.com/exfil' }).decision).toBe('block'); }); it('blocks data: URI', () => { expect(classifyMcpTool('WebFetch', { url: 'data:text/html,