- 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
178 lines
6.1 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|