shieldx/tests/unit/mcp-guard/ResourceGovernor.test.ts
Rene Fichtmueller a3793a1357 feat: ShieldX v0.1.0 — Self-Evolving LLM Prompt Injection Defense
10-layer defense pipeline with kill chain mapping, self-healing,
self-learning, and compliance reporting. Local-first, zero cloud deps.

- 72 detection rules across 7 kill chain phases
- 294 unit tests, 500+ attack corpus samples
- Management dashboard (Next.js 15, 10 pages)
- Automated resistance testing (2x daily, 31 probes)
- MITRE ATLAS, OWASP LLM Top 10, EU AI Act compliance
- Integrations: Next.js middleware, Ollama, n8n
- PostgreSQL 17 + pgvector for persistent learning
2026-03-27 15:07:27 +13: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)
})
})
})