shieldx/tests/unit/learning/ActiveLearner.test.ts
Rene Fichtmueller ca02998a28 feat: ShieldX v0.5.0 — full defense evolution + pentest hardening
4-phase defense evolution (Bio-Immune, Adversarial, Ensemble, ATLAS)
with ~200 new detection rules across 20 languages.

TPR 32.9% → 70.8%, FPR 12.2% → 0.0%

New modules: DefenseEnsemble, AtlasTechniqueMapper, EvolutionEngine,
ImmuneMemory, FeverResponse, MELONGuard, AdversarialTrainer,
DecompositionDetector, IndirectInjectionDetector, OutputPayloadGuard,
ToolCallSafetyGuard, AuthContextGuard, ResourceExhaustionDetector,
TokenizerDeobfuscation, Binary/Hex decoder, OverDefenseCalibrator
2026-04-07 00:27:12 +02:00

235 lines
8.0 KiB
TypeScript

/**
* ActiveLearner tests — exercises smart sampling and review routing logic.
* No database required — tests the stateful in-memory logic.
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { ActiveLearner } from '../../../src/learning/ActiveLearner.js'
import type { ScanResult } from '../../../src/types/detection.js'
function makeScanResult(overrides: Partial<ScanResult> = {}): ScanResult {
return {
scannerId: `scanner-${Date.now()}-${Math.random()}`,
scannerType: 'rule',
detected: true,
confidence: 0.5,
threatLevel: 'medium',
killChainPhase: 'initial_access',
matchedPatterns: ['pattern-001'],
latencyMs: 5,
...overrides,
}
}
describe('ActiveLearner', () => {
let learner: ActiveLearner
beforeEach(() => {
learner = new ActiveLearner()
})
describe('shouldRequestReview()', () => {
it('should return a boolean for any scan result', () => {
const result = makeScanResult()
const decision = learner.shouldRequestReview(result)
expect(typeof decision).toBe('boolean')
})
it('should flag uncertain confidence (0.3-0.7) for review', () => {
// A result with confidence exactly in the uncertain zone and a novel pattern
// should reliably be flagged for review
const result = makeScanResult({
confidence: 0.5,
matchedPatterns: [`novel-unique-pattern-${Math.random()}`],
})
const decision = learner.shouldRequestReview(result)
expect(decision).toBe(true)
})
it('should not throw for high confidence detections', () => {
const result = makeScanResult({ confidence: 0.99, matchedPatterns: ['jailbreak'] })
expect(() => learner.shouldRequestReview(result)).not.toThrow()
})
it('should not throw for zero confidence (false negative candidate)', () => {
const result = makeScanResult({
detected: false,
confidence: 0,
threatLevel: 'none',
killChainPhase: 'none',
matchedPatterns: [],
})
expect(() => learner.shouldRequestReview(result)).not.toThrow()
})
it('should flag a novel pattern (not seen before) for review', () => {
const uniquePattern = `novel-pattern-${Math.random()}`
const result = makeScanResult({ matchedPatterns: [uniquePattern] })
// First encounter of this pattern — should be flagged as novel
const decision = learner.shouldRequestReview(result)
expect(decision).toBe(true)
})
it('should not flag a previously seen high-confidence result for review', () => {
const seenPattern = `seen-pattern-${Math.random()}`
// First call registers the pattern as seen
learner.shouldRequestReview(
makeScanResult({ confidence: 0.99, matchedPatterns: [seenPattern] }),
)
// Second call — pattern is known, confidence is high, no feedback contradiction
const secondResult = makeScanResult({ confidence: 0.99, matchedPatterns: [seenPattern] })
const decision = learner.shouldRequestReview(secondResult)
// High confidence + already seen pattern should not be flagged
expect(decision).toBe(false)
})
it('should increment totalCount on every call', () => {
expect(learner.getReviewRate()).toBe(0)
learner.shouldRequestReview(makeScanResult({ confidence: 0.99, matchedPatterns: [] }))
learner.shouldRequestReview(makeScanResult({ confidence: 0.99, matchedPatterns: [] }))
// Rate may be 0 if nothing reviewed, but totalCount drives the denominator
const rate = learner.getReviewRate()
expect(typeof rate).toBe('number')
expect(rate).toBeGreaterThanOrEqual(0)
})
})
describe('getReviewQueue()', () => {
it('should return an array', () => {
const queue = learner.getReviewQueue()
expect(Array.isArray(queue)).toBe(true)
})
it('should start empty', () => {
expect(learner.getReviewQueue().length).toBe(0)
})
it('should contain a result after it is flagged for review', () => {
const result = makeScanResult({
scannerId: 'queue-test-scanner',
confidence: 0.5,
matchedPatterns: [`unique-${Math.random()}`],
})
learner.shouldRequestReview(result)
const queue = learner.getReviewQueue()
expect(queue.length).toBeGreaterThan(0)
})
it('should return a frozen array (immutable)', () => {
const queue = learner.getReviewQueue()
expect(Object.isFrozen(queue)).toBe(true)
})
})
describe('processReview()', () => {
it('should accept true positive verdict without throwing', () => {
expect(() => learner.processReview('scan-001', true)).not.toThrow()
})
it('should accept false positive verdict without throwing', () => {
expect(() => learner.processReview('scan-002', false)).not.toThrow()
})
it('should accept multiple review verdicts', () => {
for (let i = 0; i < 10; i++) {
expect(() => learner.processReview(`scan-${i}`, i % 2 === 0)).not.toThrow()
}
})
it('should remove a reviewed item from the queue by scannerId', () => {
const scannerId = `removable-scanner-${Math.random()}`
const result = makeScanResult({
scannerId,
confidence: 0.5,
matchedPatterns: [`novel-${Math.random()}`],
})
learner.shouldRequestReview(result)
const queueBefore = learner.getReviewQueue()
const found = queueBefore.some((r) => r.scannerId === scannerId)
expect(found).toBe(true)
learner.processReview(scannerId, true)
const queueAfter = learner.getReviewQueue()
const stillPresent = queueAfter.some((r) => r.scannerId === scannerId)
expect(stillPresent).toBe(false)
})
})
describe('getReviewRate()', () => {
it('should return 0 when no scans have been processed', () => {
expect(learner.getReviewRate()).toBe(0)
})
it('should return a number between 0 and 1', () => {
for (let i = 0; i < 20; i++) {
learner.shouldRequestReview(
makeScanResult({ confidence: 0.5, matchedPatterns: [`p-${i}`] }),
)
}
const rate = learner.getReviewRate()
expect(rate).toBeGreaterThanOrEqual(0)
expect(rate).toBeLessThanOrEqual(1)
})
})
describe('reset()', () => {
it('should clear the review queue', () => {
learner.shouldRequestReview(
makeScanResult({ confidence: 0.5, matchedPatterns: [`novel-${Math.random()}`] }),
)
expect(learner.getReviewQueue().length).toBeGreaterThan(0)
learner.reset()
expect(learner.getReviewQueue().length).toBe(0)
})
it('should reset the review rate to 0', () => {
learner.shouldRequestReview(
makeScanResult({ confidence: 0.5, matchedPatterns: [`novel-${Math.random()}`] }),
)
learner.reset()
expect(learner.getReviewRate()).toBe(0)
})
})
describe('review rate targeting', () => {
it('should flag under 30% of results when patterns are quickly exhausted', () => {
let reviewCount = 0
const total = 100
const fixedPattern = 'repeated-known-pattern'
for (let i = 0; i < total; i++) {
const result = makeScanResult({
// Use the same pattern so it becomes "seen" after the first call
confidence: 0.85,
matchedPatterns: [fixedPattern],
})
if (learner.shouldRequestReview(result)) reviewCount++
}
// After the first result marks the pattern as seen and no uncertainty/contradiction,
// subsequent high-confidence results should not be flagged
expect(reviewCount).toBeLessThan(total * 0.3)
})
it('should flag novel patterns for review (one per unique pattern)', () => {
let reviewCount = 0
const total = 20
for (let i = 0; i < total; i++) {
const result = makeScanResult({
confidence: 0.99,
matchedPatterns: [`unique-novel-${i}`],
})
if (learner.shouldRequestReview(result)) reviewCount++
}
// Each result has a brand-new pattern, so all should be flagged
expect(reviewCount).toBe(total)
})
})
})