shieldx/tests/unit/mcp-guard/ResourceGovernor.test.ts
Rene Fichtmueller 1c4c034483 feat: ShieldX v0.3.0 — UnicodeScanner (L5), DNS Covert Channel rules, ATLAS v5.4 mappings
- Layer 4 EntropyScanner: Shannon entropy, Base32/Base64 detection, CVE-2025-55284
  ping/nslookup exfil, EchoLeak markdown pattern, DNS tunneling (iodine/dnscat)
- Layer 5 UnicodeScanner: ASCII Smuggling (U+E0000 Tags Block), Variant Selectors,
  Zero-Width steganography, CamoLeak image-ordering (CVE-2025-53773), homoglyphs,
  BiDi override, high-entropy URL params
- 30 DNS covert channel rules (dns-001 to dns-030)
- ATLASMapper: 29 techniques (ATLAS v5.4.0 Feb 2026), added AML.T0062 (Agent Tool
  Invocation), AML.TA0015 (C2 tactic), memory poisoning, multi-agent trust,
  CamoLeak, Unicode steganography mappings
- Rule count: 72 → 102
- Build: tsup 316ms, zero TypeScript errors
2026-03-31 16:32:16 +02:00

178 lines
6.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest'
import {
checkBudget,
recordUsage,
getUsage,
setLimits,
clearSession,
setPricing,
} from '../../../src/mcp-guard/ResourceGovernor.js'
describe('ResourceGovernor', () => {
const sessionId = `test-rg-${Date.now()}-${Math.random()}`
beforeEach(() => {
clearSession(sessionId)
})
describe('checkBudget()', () => {
it('should allow requests within budget', () => {
const result = checkBudget(sessionId, 1000)
expect(result.allowed).toBe(true)
expect(result.remaining).toBeGreaterThan(0)
})
it('should deny requests exceeding per-request token limit', () => {
setLimits(sessionId, { maxTokensPerRequest: 500 })
const result = checkBudget(sessionId, 1000)
expect(result.allowed).toBe(false)
expect(result.reason).toContain('per-request token limit')
})
it('should deny requests when session budget is exhausted', () => {
setLimits(sessionId, { maxTokensPerSession: 100 })
recordUsage(sessionId, 50, 40, 10)
const result = checkBudget(sessionId, 20)
expect(result.allowed).toBe(false)
expect(result.reason).toContain('Session token budget exhausted')
})
it('should deny requests when cost budget is exceeded', () => {
setLimits(sessionId, { maxCostPerSession: 0.001 })
recordUsage(sessionId, 1000, 1000, 10)
const result = checkBudget(sessionId, 10000)
expect(result.allowed).toBe(false)
expect(result.reason).toContain('Cost budget exceeded')
})
})
describe('rate limiting', () => {
it('should deny requests when rate limit is exceeded', () => {
setLimits(sessionId, { maxRequestsPerMinute: 3 })
// Record 3 requests
recordUsage(sessionId, 10, 10, 1)
recordUsage(sessionId, 10, 10, 1)
recordUsage(sessionId, 10, 10, 1)
const result = checkBudget(sessionId, 10)
expect(result.allowed).toBe(false)
expect(result.reason).toContain('Rate limit exceeded')
})
it('should allow requests within rate limit', () => {
setLimits(sessionId, { maxRequestsPerMinute: 10 })
recordUsage(sessionId, 10, 10, 1)
const result = checkBudget(sessionId, 10)
expect(result.allowed).toBe(true)
})
})
describe('recordUsage()', () => {
it('should track token usage', () => {
recordUsage(sessionId, 100, 200, 50)
const usage = getUsage(sessionId)
expect(usage.totalInputTokens).toBe(100)
expect(usage.totalOutputTokens).toBe(200)
expect(usage.requestCount).toBe(1)
})
it('should accumulate usage across requests', () => {
recordUsage(sessionId, 100, 200, 10)
recordUsage(sessionId, 150, 300, 20)
const usage = getUsage(sessionId)
expect(usage.totalInputTokens).toBe(250)
expect(usage.totalOutputTokens).toBe(500)
expect(usage.requestCount).toBe(2)
})
it('should calculate cost', () => {
recordUsage(sessionId, 1000, 1000, 10)
const usage = getUsage(sessionId)
expect(usage.totalCost).toBeGreaterThan(0)
})
})
describe('ThinkTrap detection', () => {
it('should detect high output/input ratio', () => {
const warnings = recordUsage(sessionId, 10, 10000, 5000)
expect(warnings.some(w => w.includes('think_trap_detected'))).toBe(true)
})
it('should not trigger ThinkTrap for normal ratios', () => {
const warnings = recordUsage(sessionId, 1000, 2000, 100)
expect(warnings.some(w => w.includes('think_trap_detected'))).toBe(false)
})
it('should not trigger ThinkTrap for small output', () => {
// Even with high ratio, output below threshold should not trigger
const warnings = recordUsage(sessionId, 1, 100, 10)
expect(warnings.some(w => w.includes('think_trap_detected'))).toBe(false)
})
it('should include ratio in ThinkTrap warning', () => {
const warnings = recordUsage(sessionId, 100, 50000, 5000)
const thinkTrapWarning = warnings.find(w => w.includes('think_trap_detected'))
expect(thinkTrapWarning).toBeDefined()
expect(thinkTrapWarning).toContain('output/input ratio')
})
})
describe('cost estimation', () => {
it('should track cost based on token pricing', () => {
recordUsage(sessionId, 1000, 500, 10)
const usage = getUsage(sessionId)
expect(usage.totalCost).toBeGreaterThan(0)
})
it('should respect custom pricing', () => {
const customSession = `custom-pricing-${Date.now()}`
setPricing(0.00001, 0.00005)
recordUsage(customSession, 1000, 1000, 10)
const usage = getUsage(customSession)
const expectedCost = (1000 * 0.00001) + (1000 * 0.00005)
expect(usage.totalCost).toBeCloseTo(expectedCost, 4)
// Reset to defaults
setPricing(0.000003, 0.000015)
clearSession(customSession)
})
})
describe('getUsage()', () => {
it('should return zero usage for unknown session', () => {
const usage = getUsage('nonexistent-session-id')
expect(usage.totalInputTokens).toBe(0)
expect(usage.totalOutputTokens).toBe(0)
expect(usage.totalCost).toBe(0)
expect(usage.requestCount).toBe(0)
})
})
describe('budget warnings', () => {
it('should warn when approaching session token limit', () => {
setLimits(sessionId, { maxTokensPerSession: 1000 })
const warnings = recordUsage(sessionId, 450, 460, 10)
expect(warnings.some(w => w.includes('session_budget_warning'))).toBe(true)
})
it('should warn when approaching cost limit', () => {
setLimits(sessionId, { maxCostPerSession: 0.01 })
// Record enough usage to approach the limit
setPricing(0.001, 0.001)
const warnings = recordUsage(sessionId, 5, 5, 10)
expect(warnings.some(w => w.includes('cost_budget_warning'))).toBe(true)
setPricing(0.000003, 0.000015)
})
})
describe('clearSession()', () => {
it('should clear all usage data for a session', () => {
recordUsage(sessionId, 100, 200, 10)
clearSession(sessionId)
const usage = getUsage(sessionId)
expect(usage.totalInputTokens).toBe(0)
expect(usage.requestCount).toBe(0)
})
})
})