UNPKG

@iservu-inc/adf-cli

Version:

CLI tool for AgentDevFramework - AI-assisted development framework with multi-provider AI support

340 lines (255 loc) 11 kB
const { calculateDecay, getDecayRate, renewPattern, applyDecayToStoredPatterns, renewStoredPattern, getDefaultDecayConfig } = require('../lib/learning/pattern-detector'); describe('Pattern Decay Algorithm', () => { const createPattern = (overrides = {}) => ({ id: 'pattern_001', type: 'consistent_skip', questionId: 'q_deployment', confidence: 90, initialConfidence: 90, sessionsAnalyzed: 10, skipCount: 9, status: 'active', userApproved: false, createdAt: new Date('2025-01-01').toISOString(), lastSeen: new Date('2025-09-01').toISOString(), lastDecayCalculation: new Date('2025-09-01').toISOString(), timesRenewed: 0, ...overrides }); const config = getDefaultDecayConfig(); describe('getDecayRate', () => { test('high confidence (≥90) decays at half rate', () => { const highRate = getDecayRate(95, 0.15); expect(highRate).toBe(0.075); // 7.5% }); test('medium confidence (75-89) decays at normal rate', () => { const mediumRate = getDecayRate(80, 0.15); expect(mediumRate).toBe(0.15); // 15% }); test('low confidence (<75) decays at 1.5x rate', () => { const lowRate = getDecayRate(65, 0.15); expect(lowRate).toBe(0.225); // 22.5% }); }); describe('calculateDecay', () => { test('decays confidence over time', () => { const pattern = createPattern({ confidence: 90, lastSeen: new Date('2025-08-01').toISOString() }); // Mock current date to 2 months later const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const decayed = calculateDecay(pattern, config); // Should decay over 2 months expect(decayed.confidence).toBeLessThan(90); expect(decayed.lastDecayCalculation).toBe(mockDate.toISOString()); jest.restoreAllMocks(); }); test('high confidence patterns decay slower than low confidence', () => { const highPattern = createPattern({ confidence: 95, lastSeen: new Date('2025-08-01').toISOString() }); const lowPattern = createPattern({ confidence: 65, lastSeen: new Date('2025-08-01').toISOString() }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const highDecayed = calculateDecay(highPattern, config); const lowDecayed = calculateDecay(lowPattern, config); // Calculate decay percentages const highDecayPct = (highPattern.confidence - highDecayed.confidence) / highPattern.confidence; const lowDecayPct = (lowPattern.confidence - lowDecayed.confidence) / lowPattern.confidence; expect(highDecayPct).toBeLessThan(lowDecayPct); jest.restoreAllMocks(); }); test('skips decay if disabled', () => { const pattern = createPattern(); const disabledConfig = { ...config, enabled: false }; const result = calculateDecay(pattern, disabledConfig); expect(result.confidence).toBe(pattern.confidence); }); test('skips decay for user-approved patterns when protected', () => { const approvedPattern = createPattern({ userApproved: true, lastSeen: new Date('2025-06-01').toISOString() }); const protectConfig = { ...config, protectApproved: true }; const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const decayed = calculateDecay(approvedPattern, protectConfig); const normalDecayed = calculateDecay(createPattern({ userApproved: false, lastSeen: new Date('2025-06-01').toISOString() }), config); // User-approved should decay slower expect(decayed.confidence).toBeGreaterThan(normalDecayed.confidence); jest.restoreAllMocks(); }); test('skips decay if recently seen (< 1 week)', () => { const recentPattern = createPattern({ lastSeen: new Date('2025-09-28').toISOString() }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const result = calculateDecay(recentPattern, config); expect(result.confidence).toBe(recentPattern.confidence); jest.restoreAllMocks(); }); test('confidence never goes below 0', () => { const lowPattern = createPattern({ confidence: 5, lastSeen: new Date('2024-01-01').toISOString() }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const decayed = calculateDecay(lowPattern, config); expect(decayed.confidence).toBeGreaterThanOrEqual(0); jest.restoreAllMocks(); }); }); describe('renewPattern', () => { test('increases confidence by renewal boost', () => { const pattern = createPattern({ confidence: 60 }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const renewed = renewPattern(pattern, config); expect(renewed.confidence).toBe(70); // 60 + 10 (renewalBoost) expect(renewed.timesRenewed).toBe(1); expect(renewed.lastSeen).toBe(mockDate.toISOString()); jest.restoreAllMocks(); }); test('caps at initialConfidence + 5', () => { const pattern = createPattern({ confidence: 93, initialConfidence: 90 }); const renewed = renewPattern(pattern, config); expect(renewed.confidence).toBe(95); // Max is 90 + 5 }); test('never exceeds 100', () => { const pattern = createPattern({ confidence: 96, initialConfidence: 98 }); const renewed = renewPattern(pattern, config); expect(renewed.confidence).toBe(100); // Capped at 100 }); test('updates lastSeen and prevents immediate decay', () => { const pattern = createPattern(); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const renewed = renewPattern(pattern, config); expect(renewed.lastSeen).toBe(mockDate.toISOString()); expect(renewed.lastDecayCalculation).toBe(mockDate.toISOString()); jest.restoreAllMocks(); }); }); describe('applyDecayToStoredPatterns', () => { test('applies decay to all patterns', () => { const patterns = [ createPattern({ id: 'p1', confidence: 90, lastSeen: new Date('2025-08-01').toISOString() }), createPattern({ id: 'p2', confidence: 80, lastSeen: new Date('2025-08-01').toISOString() }), createPattern({ id: 'p3', confidence: 70, lastSeen: new Date('2025-08-01').toISOString() }) ]; const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const decayed = applyDecayToStoredPatterns(patterns, config); expect(decayed.length).toBe(3); expect(decayed[0].confidence).toBeLessThan(90); expect(decayed[1].confidence).toBeLessThan(80); expect(decayed[2].confidence).toBeLessThan(70); jest.restoreAllMocks(); }); test('handles empty pattern array', () => { const patterns = []; const decayed = applyDecayToStoredPatterns(patterns, config); expect(decayed).toEqual([]); }); }); describe('renewStoredPattern', () => { test('renews a single pattern', () => { const pattern = createPattern({ confidence: 65 }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const renewed = renewStoredPattern(pattern, config); expect(renewed.confidence).toBe(75); expect(renewed.timesRenewed).toBe(1); jest.restoreAllMocks(); }); }); describe('Edge Cases', () => { test('handles very old patterns (6+ months)', () => { const oldPattern = createPattern({ confidence: 80, lastSeen: new Date('2024-12-01').toISOString() }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const decayed = calculateDecay(oldPattern, config); // Should have significantly decayed after 10 months expect(decayed.confidence).toBeLessThan(40); jest.restoreAllMocks(); }); test('handles pattern oscillation (multiple renewals)', () => { let pattern = createPattern({ confidence: 70, timesRenewed: 0 }); // Simulate multiple renewals for (let i = 0; i < 3; i++) { pattern = renewPattern(pattern, config); } expect(pattern.timesRenewed).toBe(3); expect(pattern.confidence).toBeGreaterThan(70); }); test('handles missing timestamp fields gracefully', () => { const incompletePattern = { id: 'pattern_incomplete', confidence: 80, type: 'consistent_skip' // Missing lastSeen, createdAt, etc. }; // Should not throw error expect(() => { calculateDecay(incompletePattern, config); }).not.toThrow(); }); test('handles zero confidence gracefully', () => { const zeroPattern = createPattern({ confidence: 0 }); const decayed = calculateDecay(zeroPattern, config); expect(decayed.confidence).toBe(0); }); test('handles 100% confidence', () => { const maxPattern = createPattern({ confidence: 100, lastSeen: new Date('2025-08-01').toISOString() }); const mockDate = new Date('2025-10-01'); jest.spyOn(global, 'Date').mockImplementation(() => mockDate); const decayed = calculateDecay(maxPattern, config); expect(decayed.confidence).toBeLessThan(100); expect(decayed.confidence).toBeGreaterThan(0); jest.restoreAllMocks(); }); }); describe('getDefaultDecayConfig', () => { test('returns valid default configuration', () => { const defaultConfig = getDefaultDecayConfig(); expect(defaultConfig).toHaveProperty('enabled'); expect(defaultConfig).toHaveProperty('baseDecayRate'); expect(defaultConfig).toHaveProperty('minConfidenceThreshold'); expect(defaultConfig).toHaveProperty('removeBelow'); expect(defaultConfig).toHaveProperty('renewalBoost'); expect(defaultConfig).toHaveProperty('protectApproved'); expect(defaultConfig).toHaveProperty('maxInactiveMonths'); expect(defaultConfig.enabled).toBe(true); expect(defaultConfig.baseDecayRate).toBe(0.15); expect(defaultConfig.minConfidenceThreshold).toBe(50); expect(defaultConfig.removeBelow).toBe(40); }); }); });